Java tutorial
/* * Zettelkasten - nach Luhmann ** Copyright (C) 2001-2014 by Daniel Ldecke (http://www.danielluedecke.de) * * Homepage: http://zettelkasten.danielluedecke.de * * * This program 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. * * This program 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 this program; * if not, see <http://www.gnu.org/licenses/>. * * * Dieses Programm ist freie Software. Sie knnen es unter den Bedingungen der GNU * General Public License, wie von der Free Software Foundation verffentlicht, weitergeben * und/oder modifizieren, entweder gem Version 3 der Lizenz oder (wenn Sie mchten) * jeder spteren Version. * * Die Verffentlichung dieses Programms erfolgt in der Hoffnung, da es Ihnen von Nutzen sein * wird, aber OHNE IRGENDEINE GARANTIE, sogar ohne die implizite Garantie der MARKTREIFE oder * der VERWENDBARKEIT FR EINEN BESTIMMTEN ZWECK. Details finden Sie in der * GNU General Public License. * * Sie sollten ein Exemplar der GNU General Public License zusammen mit diesem Programm * erhalten haben. Falls nicht, siehe <http://www.gnu.org/licenses/>. */ package de.danielluedecke.zettelkasten.util; import de.danielluedecke.zettelkasten.ToolbarIcons; import de.danielluedecke.zettelkasten.ZettelkastenView; import de.danielluedecke.zettelkasten.database.BibTex; import de.danielluedecke.zettelkasten.database.Daten; import de.danielluedecke.zettelkasten.database.DesktopData; import de.danielluedecke.zettelkasten.database.Settings; import de.danielluedecke.zettelkasten.database.Synonyms; import java.awt.Desktop; import java.awt.Frame; import java.awt.HeadlessException; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.KeyEvent; import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Handler; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.JEditorPane; import javax.swing.JOptionPane; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.parser.ParserDelegator; import org.jdom2.Element; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; /** * * @author danielludecke */ public class Tools { public static final int UBB2MARKDOWN = 1; public static final int MARKDOWN2UBB = 2; public static final String SEGMENT_POSITION_FIRST = "first"; public static final String SEGMENT_POSITION_MIDDLE = "middle"; public static final String SEGMENT_POSITION_LAST = "last"; public static final String SEGMENT_POSITION_ONLY = "only"; /** * get the strings for file descriptions from the resource map */ private final static org.jdesktop.application.ResourceMap resourceMap = org.jdesktop.application.Application .getInstance(de.danielluedecke.zettelkasten.ZettelkastenApp.class).getContext() .getResourceMap(ZettelkastenView.class); /** * get the strings for file descriptions from the resource map */ private final static org.jdesktop.application.ResourceMap toolbarResourceMap = org.jdesktop.application.Application .getInstance(de.danielluedecke.zettelkasten.ZettelkastenApp.class).getContext() .getResourceMap(ToolbarIcons.class); /** * */ private static boolean validhtml; /** * This method retrieves system information like the operating system and version, the architecture, * the used java runtime environment and the official vendor of the jre, and the java home directory. * @return a string with the above described system information */ public static String getSystemInformation() { StringBuilder sysinfo = new StringBuilder(""); sysinfo.append(System.getProperty("os.name")).append(" ").append(System.getProperty("os.version")) .append(" (").append(System.getProperty("os.arch")).append(")") .append(System.getProperty("line.separator")); sysinfo.append("Java-Version ").append(System.getProperty("java.version")).append(" (") .append(System.getProperty("java.vendor")).append(")").append(System.getProperty("line.separator")); sysinfo.append(System.getProperty("java.home")); return sysinfo.toString(); } /** * This method converts Zettelkasten ubb-format-chars like {@code [br]} or {@code 	} into * UTF-chars for a regular input in a text field (like \n or \t). * * @param text the text that contains UBB-tags like {@code [br]} or {@code 	} * @return a string with converted UTF-chars (line separators, tabs) used in a regular text field */ public static String replaceUbbToUnicode(String text) { if (text != null && !text.isEmpty()) { // replace all "br" with "real" new lines text = text.replace("[br]", System.getProperty("line.separator")); // replace all "tabs" with "real" tabs text = text.replace("	", "\t"); // replace all bullet-codes with "real" bullets text = text.replace("•", String.valueOf((char) 8226)); } return text; } /** * This method converts UTF-chars from a regular input or text field (like \n or \t) * into its Zettelkasten ubb-format. * <br><br> * Use this method if you want to make the content of {@code text} compatible with * the Zettelkasten's UBB-tags, i.e. if you need the content of {@code text} for * creating a new entry. * * @param text the text that contains UTF chars like line separators or tabs * @return a string with converted ubb-chars (line separators become {@code [br]}, tabs * {@code 	} etc. */ public static String replaceUnicodeToUbb(String text) { if (text != null && !text.isEmpty()) { // check whether the line-seperator has 2 chars. if so, remove first char... if (System.getProperty("line.separator").contains("\r")) { text = text.replace("\r", ""); } // ...only then this replacement will work text = text.replace("\n", "[br]"); // replace all "tabs" with "real" tabs text = text.replace("\t", "	"); // replace all bullet-codes with "real" bullets text = text.replace(String.valueOf((char) 8226), "•"); } return text; } /** * * @param linktype * @param data * @param displayedZettel * @return */ public static boolean handleHyperlink(String linktype, Daten data, int displayedZettel) { // here we have a cross reference to another entry if (linktype.startsWith("#cr_")) { // only remove manual links from activated entry! if (displayedZettel != data.getCurrentZettelPos()) return false; try { // remove manual link data.deleteManualLinks(new String[] { linktype.substring(4) }); return true; } catch (IndexOutOfBoundsException e) { Constants.zknlogger.log(Level.WARNING, e.getLocalizedMessage()); } } return false; } /** * This method opens a file or URL either from within a clicked link inside the jEditorPane (see * {@link #eventHyperlinkActivated(javax.swing.event.HyperlinkEvent) eventHyperlinkActivated(javax.swing.event.HyperlinkEvent)} * or from the attachment-list (see {@link #openAttachment() openAttachment()}. * * This method is called from the ZettelkastenView.class, the CDesktop.class and the CSearchResults.class. * * @param linktype a String value containing the URL of the clicked hyperlink * @param frame the frame which was the source from the editor pane that contained the hyperlink * @param sourceframe a reference to the frame from where this function call came. needed for * the html-formatting, since entries are differently formatted in the search window. * @param data a reference to the CDaten class * @param bibtexObj * @param settings a reference to the CSettings class * @param mainpane a reference to the JEditorPane that was the source of the hyperlink-event * @param displayedZettel the currently displayed entry. does not apply to the CDesktop.class * @return <ul> * <li>in case a literatur footnote was clicked, the related author-value is returned as string value.</li>> * <li>in case a rating-star was clicked, the value {@code #rateentry} with appended entry-number is returned.</li> * <li>in all other cases, {@code null} is returned.</li> * </ul> */ public static String openHyperlink(String linktype, Frame frame, int sourceframe, Daten data, BibTex bibtexObj, Settings settings, JEditorPane mainpane, int displayedZettel) { // here comes the part that is not depending on the desktop-api // // here we have a reference (ankh) to the attachments, which are located at the // end of an entry if (linktype.equals("#hyperjump")) { mainpane.scrollToReference("hyperjump"); } else if (linktype.equals("#activatedEntry")) { return linktype; } // here we have a literature footnote. if this link is activated, we want // to display the related author in the tabbed pane // otherwise try to open the file with the associated application else if (linktype.startsWith("#fn_")) { try { String au = ""; // check sourceframe if (sourceframe != Constants.FRAME_DESKTOP) { // get and convert number int aunr = Integer.parseInt(linktype.substring(4)); // get author from author-xml-file au = data.getAuthor(aunr); // create a new instance of the CHtml-class for highlighting HtmlUbbUtil.setHighlighTerms(new String[] { Pattern.quote(au) }, HtmlUbbUtil.HIGHLIGHT_STYLE_LIVESEARCH, false); // get the authors with new highlighted author that was selected from the footnote String text = HtmlUbbUtil.getEntryAsHTML(settings, data, bibtexObj, displayedZettel, null, sourceframe); // set new text mainpane.setText(text); } // scroll to author in authortextfield mainpane.scrollToReference(linktype.substring(1)); // when the user also wants to have the related author selected in the // jTableAuthors (see settings), do this now if (settings.getJumpFootnote()) { return au; } } catch (NumberFormatException e) { Constants.zknlogger.log(Level.WARNING, e.getLocalizedMessage()); } } // here we have a reference to another entry else if (linktype.startsWith("#z_")) { try { // get and convert number int znr = Integer.parseInt(linktype.substring(3)); // show entry if (data.zettelExists(znr) && !data.isDeleted(znr)) { data.setCurrentZettelPos(znr); } // return String return linktype; } catch (NumberFormatException e) { Constants.zknlogger.log(Level.WARNING, e.getLocalizedMessage()); } } // here we have a cross reference to another entry else if (linktype.startsWith("#cr_")) { try { // get and convert number int znr = Integer.parseInt(linktype.substring(4)); // show entry if (data.zettelExists(znr) && !data.isDeleted(znr)) { data.setCurrentZettelPos(znr); } // return String return linktype; } catch (NumberFormatException e) { Constants.zknlogger.log(Level.WARNING, e.getLocalizedMessage()); } } // here we have a rating-request, i.e. the user clicked on the rating stars // and wants to rate the entry else if (linktype.startsWith("#rateentry")) { return linktype; } // here we have a edit cross reference else if (linktype.equalsIgnoreCase("#crt")) { return linktype; } // here we have a request for editing the timestamp else if (linktype.startsWith("#tstamp")) { return linktype; } // TODO enable if movie-player is installed/possible. // here we have a movie-url. if this link is activated, we want // to play a moview in the hud-control-window (see CMoviePlayer). // else if (linktype.startsWith("#mov")) { // CMoviePlayer movplay = new CMoviePlayer(linktype.substring(4)); // movplay.showPlayer(); // } // here comes the part which depends on the desktop-api else { launchFile(linktype, frame, data, settings); } return null; } /** * * @param linktype * @param frame * @param data * @param settings */ public static void launchFile(String linktype, Frame frame, Daten data, Settings settings) { // check whether linktype is a hyperlink if (FileOperationsUtil.isHyperlink(linktype)) { try { if (settings.getUseXDGOpen() && PlatformUtil.isLinux()) { Runtime.getRuntime().exec("xdg-open " + linktype); Constants.zknlogger.log(Level.INFO, "Using xdg-open with URL {0}", linktype); } // check whether desktop is supported else if (Desktop.isDesktopSupported()) { // get the desktop Desktop desk = Desktop.getDesktop(); // if yes, proceed... // check whether opening a browser is supported or not... if (!desk.isSupported(Desktop.Action.BROWSE)) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkUnsopportedMsg"), resourceMap.getString("errLinkUnsopportedTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.WARNING, "Desktop.Action.BROWSE not supported!"); } desk.browse(new URI(linktype)); Constants.zknlogger.log(Level.INFO, "Using desktop api with URL {0}", linktype); } // check whether we have windows os. if yes, use runtime exec instead of desktop else { Constants.zknlogger.log(Level.WARNING, "Desktop-API not supported!"); if (PlatformUtil.isWindows()) { Runtime.getRuntime().exec("rundll32 SHELL32.DLL,ShellExec_RunDLL \"" + linktype + "\""); Constants.zknlogger.log(Level.INFO, "Using rundll32 with URL {0}", linktype); // Runtime.getRuntime().exec("cmd /c start \""+linktype+"\""); } else if (PlatformUtil.isLinux()) { Runtime.getRuntime().exec("xdg-open " + linktype); Constants.zknlogger.log(Level.INFO, "Using xdg-open with URL {0}", linktype); } } } catch (IOException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkNotFoundMsg", linktype), resourceMap.getString("errLinkNotFoundTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } catch (URISyntaxException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkSyntaxMsg"), resourceMap.getString("errLinkSyntaxTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } catch (SecurityException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkNoAccessMsg"), resourceMap.getString("errLinkNoAccessTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } } // linktype seems to be a file else { File linkfile; File linuxpath; // create file from the link linkfile = FileOperationsUtil.getLinkFile(settings, data, linktype); // create path for linux with "file://" at beginning of string linuxpath = linkfile; // check whether path contains space chars if (linuxpath.toString().contains(" ")) { // if it fails, try to convert spaces with "%20" linuxpath = new File( linuxpath.toString().replaceAll(Pattern.quote(" "), Matcher.quoteReplacement("%20"))); } try { // check whether desktop-api is supported if (Desktop.isDesktopSupported()) { // get the desktop Desktop desk = Desktop.getDesktop(); // if yes, proceed... // if the string starts with "mailto:", we assume we have an email-link if (linktype.startsWith("mailto:")) { // check whether opening a browser is supported or not... if (!desk.isSupported(Desktop.Action.MAIL)) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkUnsopportedMsg"), resourceMap.getString("errLinkUnsopportedTitle"), JOptionPane.PLAIN_MESSAGE); } desk.mail(new URI(linktype)); } else { // check whether opening a file is supported or not if (!desk.isSupported(Desktop.Action.OPEN) || PlatformUtil.isWindows()) { // check whether we have windows os. if yes, use runtime exec instead of desktop if (PlatformUtil.isWindows()) { Runtime.getRuntime().exec( "rundll32 SHELL32.DLL,ShellExec_RunDLL \"" + linkfile.toString() + "\""); Constants.zknlogger.log(Level.INFO, "Using rundll32 with filepath {0}", linkfile); } else if (PlatformUtil.isLinux()) { Runtime.getRuntime().exec("xdg-open file://" + linuxpath.getPath()); Constants.zknlogger.log(Level.INFO, "Using xdg-open with filepath {0}", linuxpath.getPath()); } } else { if (PlatformUtil.isLinux()) { if (settings.getUseXDGOpen()) { Runtime.getRuntime().exec("xdg-open file://" + linuxpath.getPath()); Constants.zknlogger.log(Level.INFO, "Using xdg-open with filepath {0}", linuxpath.getPath()); } else { desk.open(linkfile); Constants.zknlogger.log(Level.INFO, "Using dekstop api with filepath {0}", linkfile.getPath()); } } else { desk.open(linkfile); Constants.zknlogger.log(Level.INFO, "Using desktop api with filepath {0}", linkfile.getPath()); } } } } // check whether we have windows os. if yes, use runtime exec instead of desktop else { Constants.zknlogger.log(Level.WARNING, "Desktop-API not supported!"); if (PlatformUtil.isWindows()) { Runtime.getRuntime().exec("rundll32 SHELL32.DLL,ShellExec_RunDLL \"" + linktype + "\""); Constants.zknlogger.log(Level.INFO, "Using rundll32 with filepath {0}", linktype); // Runtime.getRuntime().exec("cmd /c start \""+linktype+"\""); } else if (PlatformUtil.isLinux()) { Runtime.getRuntime().exec("xdg-open file://" + linuxpath.getPath()); Constants.zknlogger.log(Level.INFO, "Using xdg-open with filepath {0}", linuxpath.getPath()); } } } catch (IOException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkNotFoundMsg", (linkfile != null) ? linkfile.toString() : resourceMap.getString("linkFileUnknown")), resourceMap.getString("errLinkNotFoundTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } catch (IllegalArgumentException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkNotFoundMsg", (linkfile != null) ? linkfile.toString() : resourceMap.getString("linkFileUnknown")), resourceMap.getString("errLinkNotFoundTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } catch (SecurityException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkNoAccessMsg"), resourceMap.getString("errLinkNoAccessTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } catch (HeadlessException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkUnsopportedMsg"), resourceMap.getString("errLinkUnsopportedTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } catch (URISyntaxException e) { // display error message box JOptionPane.showMessageDialog(frame, resourceMap.getString("errLinkSyntaxMsg"), resourceMap.getString("errLinkSyntaxTitle"), JOptionPane.PLAIN_MESSAGE); Constants.zknlogger.log(Level.SEVERE, e.getLocalizedMessage()); } } } /** * This method checks the content of {@code content} for valid HTML and returns {@code true} * if the content could be parsed t HTML. With this, we check whether an entry makes use of correct * or irregular nested tags. * * @param content the html-page which should be checked for correctly nested tags, usually an entry's content * @param zettelnummer the number of the entry that is checked for valid html-tags * @return {@code true} when the content could be successfully parsed to HTML, false otherwise */ public static boolean isValidHTML(String content, final int zettelnummer) { // check for valid html validhtml = true; // first, we parse the created web-page to catch errors that might occure when parsing // the entry-content. this might happen when tags are not properly used. HTMLEditorKit.ParserCallback callback = new HTMLEditorKit.ParserCallback() { // in case the parsing was not succssful, log that error message. @Override public void handleError(String errorMsg, int pos) { if (errorMsg.toLowerCase().contains("unmatched") || errorMsg.toLowerCase().contains("missing")) { // if body tag is missing (which is true for all entries), don't log that message if (!errorMsg.toLowerCase().contains("body")) { // tell function that HTML is invalid. validhtml = false; errorMsg = System.getProperty("line.separator") + "Error when parsing the entry " + String.valueOf(zettelnummer) + "!" + System.getProperty("line.separator") + errorMsg + System.getProperty("line.separator"); Constants.zknlogger.log(Level.SEVERE, errorMsg); } } } }; // create a string-reader that reads the entry's html-content Reader reader = new StringReader(content); // try to parse the html-page try { new ParserDelegator().parse(reader, callback, false); } catch (IOException ex) { Constants.zknlogger.log(Level.WARNING, ex.getLocalizedMessage()); } return validhtml; } /** * This method cleans an HTML-entry from irregular nested tags by removing all [c] and [m] formatting tags. * @param dummy the entry's content that should be cleaned * @return the cleaned HTML-content for that entry. */ public static String cleanHTML(String dummy) { // center alignment: [c] becomes <center> dummy = dummy.replaceAll("\\[c\\](.*?)\\[/c\\]", "$1"); dummy = dummy.replaceAll("\\[m ([^\\[]*)\\](.*?)\\[/m\\]", "$2"); return dummy; } /** * This method converts the timestamp-date into a readable format. The timestamp * of entries is provided as "yymmddhhmm", e.g. "0811051810" stands for "5th November 2008, 10:18 am" * * @param d the timestamp in original format * @param shortdate * @return the timestamp in a "readable" format */ public static String getProperDate(String d, boolean shortdate) { // create an array with month names. we do this, because now we can // just edit the property-file for other languages/locales of the month-names String[] months = { resourceMap.getString("monthJan"), resourceMap.getString("monthFeb"), resourceMap.getString("monthMar"), resourceMap.getString("monthApr"), resourceMap.getString("monthMay"), resourceMap.getString("monthJun"), resourceMap.getString("monthJul"), resourceMap.getString("monthAug"), resourceMap.getString("monthSep"), resourceMap.getString("monthOct"), resourceMap.getString("monthNov"), resourceMap.getString("monthDec"), }; // prepare the return-value StringBuilder retval = new StringBuilder(""); // first, set the day. the day starts at position 4 within the string, always two digits // add a period and space behind it // here we first convert the substring into an int-value and than re-convert it to a string, // because by doing so we can have for instance "5" instead of "05", i.e. getting rid of // a leading zero. // // to check whether we are inside the boundaries when looking for the substring // we check for the length of the string if (d.length() >= 6) { try { // get the day from the timestamp int day = Integer.parseInt(d.substring(4, 6)); retval.append(String.valueOf(day)).append(". "); // now convert the month // therefore, retrieve the substring, convert it to an int-value and use this value // as index for the month array String mon = months[Integer.parseInt(d.substring(2, 4)) - 1]; if (shortdate) { mon = mon.substring(0, 3); } retval.append(mon).append(" "); // now the year, assuming that we will have the 21st century // fix this, when the new century 2100 begins :-) if (!shortdate) retval.append("20"); retval.append(d.substring(0, 2)); if (!shortdate) retval.append(", "); } catch (NumberFormatException ex) { Constants.zknlogger.log(Level.WARNING, ex.getLocalizedMessage()); } catch (IndexOutOfBoundsException ex) { Constants.zknlogger.log(Level.WARNING, ex.getLocalizedMessage()); } } // to check whether we are inside the boundaries when looking for the substring // we check for the length of the string // and finally the time if (d.length() >= 10 && !shortdate) { retval.append(d.substring(6, 8)).append(":").append(d.substring(8, 10)); } // return the final string return retval.toString(); } /** * This method converts the timestamp-date into a readable short format. The timestamp * of entries is provided as "yymmddhhmm", e.g. "0811051810" stands for "05.11.2008" 18:10 * * @param d the timestamp in original format * @return the timestamp in a "readable" format */ public static String getProperShortDate(String d) { // prepare the return-value StringBuilder retval = new StringBuilder(""); // first, set the day. the day starts at position 4 within the string, always two digits // add a period and space behind it // here we first convert the substring into an int-value and than re-convert it to a string, // because by doing so we can have for instance "5" instead of "05", i.e. getting rid of // a leading zero. // // to check whether we are inside the boundaries when looking for the substring // we check for the length of the string if (d.length() >= 6) { try { // get the day from the timestamp int day = Integer.parseInt(d.substring(4, 6)); retval.append(String.format("%02d", day)).append("."); // now convert the month // therefore, retrieve the substring, convert it to an int-value and use this value // as index for the month array int mon = Integer.parseInt(d.substring(2, 4)); retval.append(String.format("%02d", mon)).append("."); // now the year, assuming that we will have the 21st century // fix this, when the new century 2100 begins :-) retval.append("20").append(d.substring(0, 2)).append(" "); // check for valid length if (d.length() >= 10) { // get the hour from the timestamp int hour = Integer.parseInt(d.substring(6, 8)); retval.append(String.format("%02d", hour)).append(":"); // get the minutes from the timestamp int min = Integer.parseInt(d.substring(8, 10)); retval.append(String.format("%02d", min)); } } catch (NumberFormatException ex) { Constants.zknlogger.log(Level.WARNING, ex.getLocalizedMessage()); } catch (IndexOutOfBoundsException ex) { Constants.zknlogger.log(Level.WARNING, ex.getLocalizedMessage()); } } // return the final string return retval.toString(); } /** * This methods retrieves entry-numbers from an input-field, where the user can enter certain entry-numbers, * comma-separated, including ranges.<br><br> * E.g. if the user enters following: "3,5,7-9", then this method would return an integer-array containing * the values 3,5,7,8,9. * * @param input the user-input from a textfield, given as string * @param len the length of the dataset, so we don't have any entry-numbers out of bounds * @return an integer-array with the converted entry-numbers from the user input, or null if an error occured or * the input contained invalid numbers. */ public static int[] retrieveEntryNumbersFromInput(String input, int len) { // parse input at each comma String[] entries = input.split(","); // create linked list that will contain all entries... List<Integer> finalentries = new ArrayList<Integer>(); // go through all parts of the input for (String e : entries) { // remove leading/trailing space chars e = e.trim(); // if we find a "-", add all entries from lower to upper range if (e.contains("-")) { // find the position of the divider int pos = e.indexOf("-"); if (pos != -1) { try { // get the value befor that divider String lowerS = e.substring(0, pos).trim(); // and get the value behind that divider String upperS = e.substring(pos + 1).trim(); // convert strings to integer int from = Integer.parseInt(lowerS); int to = Integer.parseInt(upperS); // only proceed, when lower range is smaller than upper range if ((from < to) && (from > 0) && (to <= len)) { // now add all entries from lower to higher to the bullet for (int cnt = from; cnt <= to; cnt++) { finalentries.add(cnt); } } else { // tell user about invalid value JOptionPane.showMessageDialog(null, resourceMap.getString("errInvalidValueMsg"), resourceMap.getString("errInvalidValueTitle"), JOptionPane.PLAIN_MESSAGE); return null; } } catch (NumberFormatException ex) { // tell user about invalid value JOptionPane.showMessageDialog(null, resourceMap.getString("errInvalidValueMsg"), resourceMap.getString("errInvalidValueTitle"), JOptionPane.PLAIN_MESSAGE); return null; } catch (IndexOutOfBoundsException ex) { // tell user about invalid value JOptionPane.showMessageDialog(null, resourceMap.getString("errInvalidValueMsg"), resourceMap.getString("errInvalidValueTitle"), JOptionPane.PLAIN_MESSAGE); return null; } } } else { try { // convert string to integer int nr = Integer.parseInt(e); // check for valid entry... if (nr < 0 || nr > len) { // tell user about invalid value JOptionPane.showMessageDialog(null, resourceMap.getString("errInvalidValueMsg"), resourceMap.getString("errInvalidValueTitle"), JOptionPane.PLAIN_MESSAGE); return null; } // else add nr finalentries.add(nr); } catch (NumberFormatException ex) { // tell user about invalid value JOptionPane.showMessageDialog(null, resourceMap.getString("errInvalidValueMsg"), resourceMap.getString("errInvalidValueTitle"), JOptionPane.PLAIN_MESSAGE); return null; } } } // create return value int[] retval = null; // check whether we have any entries if (finalentries.size() > 0) { // init return value retval = new int[finalentries.size()]; // copy entries from list to array for (int cnt = 0; cnt < retval.length; cnt++) { retval[cnt] = finalentries.get(cnt); } } // return result return retval; } /** * This methods checks whether a string contains transfer-data from a table, and if so, extracts * the entry-numbers from this string.<br><br> * A string copied or dragged from a jTable may consist of several lines, where each line has * an entrynumber (first cell), a tab as separator char and a title (as 2nd cell).<br><br> * Thus, a dropped string might look like this:<br> * {@code 3 This is the third entry}<br> * {@code 6 This is number six}<br> * {@code 9 My last entry} * * @param str the string that was dropped or pasted, i.e. received by the transfer handler * @param maxcount * @return an integer-array with all entry-numbers that could be extracted, or {@code null} * if the string did not contain any entrynumbers. */ public static int[] retrieveEntryNumbersFromTransferHandler(String str, int maxcount) { // check whether we have any valid string if (str != null) { // create integer list for entry numbers ArrayList<Integer> entries = new ArrayList<Integer>(); // split drop-string at each new line String[] lines = str.split("\n"); // iterate all line for (String line : lines) { // if line is not empty, split it at each cell (tab-separated) if (!line.isEmpty()) { // retrieve cell-data String[] cells = line.split("\t"); // if we have any cells, go on if (cells.length > 0) { try { // try to convert data of each first cell into an integer int e = Integer.parseInt(cells[0]); // if succeeded, and entry is within valid range, // add it to list if (e > 0 && e <= maxcount) { entries.add(e); } } catch (NumberFormatException ex) { } } } } // if there were any entries in the drop-data, copy them to an int-array if (entries.size() > 0) { int[] retval = new int[entries.size()]; for (int cnt = 0; cnt < entries.size(); cnt++) { retval[cnt] = entries.get(cnt); } return retval; } } return null; } /** * This method retrieves multiple occurences of the entries that are passed in the linked list * {@code addedEntries} and puts them together to a linked list of type {@code Object []}. * * @param desktopObj the reference to the CDesktopData-class * @param addedEntries a linked list of integer values, containing all entry-numbers that should be * looked after for multiple occurences * @return a linked list of {@code Object []}, where each object-array has the 2 fields: {@code Object[0]} * containing the desktop-name as string, and {@code Object[1]} a linked list of type {@code Element} that * contains all found entries as Element-values. */ public static List<Object[]> retrieveDoubleEntries(DesktopData desktopObj, List<Integer> addedEntries) { // create a linked list that will scan for multiple occurences of the added entries List<Object[]> multipleentries = new ArrayList<Object[]>(); // first we go through all saved desktops we have and look // for occurences of the to be added entry. if the entry exists // in one or more desktops, the element is stored in our linked list. // if the entry does not exist, "null" is added to our linked list. // below we can than inform the user about multiple occurences of entries, // their location in the desktops and the timestamp when that entry was // added in the past for (int me = 0; me < desktopObj.getCount(); me++) { // create a new object array for the relevant data Object[] o = new Object[2]; // store the desktopname o[0] = desktopObj.getDesktopName(me); // now search for double entries (of those added entries) within this desktop // and store them in the array Iterator<Integer> it = addedEntries.iterator(); // since we may find multiple entries, we create a new linked // list here, that will store *all* found entries of the current desktop List<Element> finallist = new ArrayList<Element>(); // go through all found entries... // what we do here is following: we may have several different entries that already // have been added to this desktop before. these entries, if we have any, are // stored in the linked list "addedEntries". Now we have to iterate this list for // each existing desktop. each entry of the list "addedEntries" may appear one or more(!) // times in a desktop (e.g., the entry "16" may occure in several sub-bullets). // so calling the function "desktopObj.searchForEntry(me, it.next())" may return // several(!) Elements in a linked list for each(!) entry. thus, we may have to // concatenate several linked lists together. this is achieved by iterating the // linked list "lle" and adding each element of it to the resulting linked list "finallist" // for each entry within the found entries list "addedEntries". // finally, if we have any elements in our "finallist", it is added to the object-array "o". while (it.hasNext()) { // retrieve all found entries within the desktop List<Element> lle = desktopObj.searchForEntry(me, it.next()); // check whether we have any returned entries... if (lle != null && lle.size() > 0) { // create a new iterator for the found results Iterator<Element> prepare = lle.iterator(); // iterate them while (prepare.hasNext()) { // get each single entry as element Element e = prepare.next(); // and add it to the final list if (e != null) { finallist.add(e); } } } } o[1] = (finallist.size() > 0) ? finallist : null; // add the object to our linked list, so this list will contain the desktop-name // and the possible double entries als elements. multipleentries.add(o); } return multipleentries; } /** * This method prepares a message that tells the user which entries already appear in the desktop, and * at which position. the complete message is returned as string. * * @param list a linked list which contains the multiple-entry-data. see * {@link #retrieveDoubleEntries(zettelkasten.CDesktopData, java.util.LinkedList) retrieveDoubleEntries(zettelkasten.CDesktopData, java.util.LinkedList)} * for more details on how this parameter is created. use the return result of this method as this parameter * @return a string with the message which entries are at which position in the desktop-data, or {@code null} * if no occurences appear. */ public static String prepareDoubleEntriesMessage(List<Object[]> list) { // retrieve system's line-separator String lineseparator = System.getProperty("line.separator"); // get an iterator for the multiple entries and check // whether we have any multiple occurences at all. if yes, // tell the user about that Iterator<Object[]> i = list.iterator(); // prepare a string builder that will contain the information-message in case // we have any multiple occurences of entries... StringBuilder multipleOccurencesMessage = new StringBuilder(""); // go through all entries of the linked list and check // whether we have found anything while (i.hasNext()) { // get element Object[] desktopdata = i.next(); // if second element in array is not null, we have a match. now retrieve // the entry's data, so we can inform the user about the // entry's details... if (desktopdata[1] != null) { // retrieve desktop name String dn = resourceMap.getString("multipleOccurencesDesktop") + " " + (String) desktopdata[0]; StringBuilder dnsl = new StringBuilder(""); // now we add a separator line, so check length of string for (int dnl = 0; dnl < dn.length(); dnl++) dnsl.append("-"); // first, append desktop-name multipleOccurencesMessage.append(dn).append(lineseparator); multipleOccurencesMessage.append(dnsl.toString()).append(lineseparator); // now retrieve the elements... List<Element> elements = (ArrayList<Element>) desktopdata[1]; // create iterator for each found element Iterator<Element> entryIterator = elements.iterator(); // go through the found entries in that desktop while (entryIterator.hasNext()) { // get each found entry as element Element entry = entryIterator.next(); // get the timestamp of the found entry String timestamp = entry.getAttributeValue("timestamp"); // get the entrynumber of the found entry String id = entry.getAttributeValue("id"); // create a linked list that will hold the path to the desktop List<String> path = new ArrayList<String>(); // as long as the found element has parents, we have path-elements/information // to add... while (entry.getParentElement() != null) { // retrieve parent-element entry = entry.getParentElement(); // if it's a bullet, add the path-name to our path-list if (entry.getName().equals("bullet")) { path.add(0, entry.getAttributeValue("name")); } } // now we can prepare the output string... multipleOccurencesMessage.append( resourceMap.getString("multipleOccurencesMsg", id, getProperDate(timestamp, false))); multipleOccurencesMessage.append(lineseparator) .append(resourceMap.getString("multipleOccurencesLevel")).append(" "); // go through the path-list and append all path-elements, so the user // knows where to find the entry for (int cnt = 0; cnt < path.size(); cnt++) { // add path multipleOccurencesMessage.append(path.get(cnt)); // as long as we have a path-element left, append a separating comma if (cnt < path.size() - 1) { multipleOccurencesMessage.append(" >>> "); } } // append two line-separators for the next element... multipleOccurencesMessage.append(lineseparator).append(lineseparator); } } } // delete the last two trailing lineseparators if (multipleOccurencesMessage.length() > 0) { multipleOccurencesMessage.setLength(multipleOccurencesMessage.length() - 2 * lineseparator.length()); } // if we have any content, return string. else return null return (multipleOccurencesMessage.length() > 0) ? multipleOccurencesMessage.toString() : null; } /** * This method checks whether a list of {@code keywords} contains values that matches a synonym in * the synonyms-datafile, where the keyword-value is a synonym, but <i>no</i> index-word. If synonyms in the * {@code keywords}-array have been found, the user is asked whether he wants to replace these synonyms * by their index-words (which is recommended to do so, because keywords in general should be the * index-word of a synonym-line). * * @param synonymsObj a reference to the synonyms-data-class {@code CSynonyms} * @param keywords an array of keywords that should be checked, whether one or more elements of this * array are synonyms, but no index-word * @return if the user agrees, a "cleaned" array containing only keywords which appear as index-words of * synonyms only, or the original array which was passed as parameter if the user does not agree to * do the replacement. In case the user <i>cancelled</i> the action, {@code null} is returned. */ public static String[] replaceSynonymsWithKeywords(Synonyms synonymsObj, String[] keywords) { // create linked list that will hold the keywords which have been recognized as synonyms, but not // as index-words List<String> synkeywords = new ArrayList<String>(); // check keywords whether they appear as synonyms, but not index-word for (String skw : keywords) { if ((synonymsObj.findSynonym(skw, true) != -1) && !synonymsObj.isIndexWord(skw, true)) { synkeywords.add(skw); } } // if we have any matches, go on here if (synkeywords.size() > 0) { // prepare option-message StringBuilder msg = new StringBuilder(""); for (String synkeyword : synkeywords) { msg.append("<br>" + "- <b>").append(synkeyword).append("</b> <i>(Index-Synonym: ") .append(synonymsObj.getIndexWord(synkeyword, true)).append(")</i>"); } // show option pane int option = JOptionPane.showConfirmDialog(null, resourceMap.getString("replaceSynonymsWithKeywordsMsg", msg.toString()), resourceMap.getString("replaceSynonymsWithKeywordsTitle"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); // if the user wants to replace the keyword-synonyms by their index-word, do this now if (JOptionPane.YES_OPTION == option) { // create new list for return value ArrayList<String> newkeywords = new ArrayList<String>(); for (String keyword : keywords) { // and replace all found synonyms-values by their related index-words if ((synonymsObj.findSynonym(keyword, true) != -1) && !synonymsObj.isIndexWord(keyword, true)) { // retrieve index-word String iword = synonymsObj.getIndexWord(keyword, true); // check whether replaced index-word already exists in keywords. by replacing a synonym with its // index-word, we might produce duplicate keywords, in case the index-word already existed before. // keywords[cnt] = iword; if (!newkeywords.contains(iword)) { newkeywords.add(iword); } } else { if (!newkeywords.contains(keyword)) { newkeywords.add(keyword); } } } // copy list to array keywords = newkeywords.toArray(new String[newkeywords.size()]); } else if (JOptionPane.CANCEL_OPTION == option || JOptionPane.CLOSED_OPTION == option /*User pressed cancel key*/) { return null; } } return keywords; } /** * This method sets the locale descriptions for the standard-actions cut, copy and paste - * which are in English by default. * @param actionMap the class's actionmap */ public static void initLocaleForDefaultActions(javax.swing.ActionMap actionMap) { String[] actions = new String[] { "cut", "copy", "paste" }; for (String ac : actions) { // get the action's name AbstractAction aac = (AbstractAction) actionMap.get(ac); // and put them together :-) aac.putValue(AbstractAction.NAME, toolbarResourceMap.getString(ac + ".Action.text")); aac.putValue(AbstractAction.SHORT_DESCRIPTION, toolbarResourceMap.getString(ac + ".Action.shortDescription")); // // get the new icon-path from the resource-map // URL imgURL = org.jdesktop.application.Application.getInstance(de.danielluedecke.zettelkasten.ZettelkastenApp.class).getClass().getResource(resourceMap.getString(ac+".Action.largeIcon")); // // create icon // ImageIcon img = new ImageIcon(imgURL); // aac.putValue(AbstractAction.LARGE_ICON_KEY, img); aac.putValue(AbstractAction.SMALL_ICON, null); } } /** * This method retrieves a string-array of keywords and separates them at certain delimiter-chars * like comma, slash, minus, plus and space. Then, from all keyword-parts particular words are * removed, like "and", "the", "one" etc. The remaining parts of the keyword(s) are copied to a * string array and returned. * * @param keywords a string-array with keywords that should be separated * @param matchcase {@code true} if the case should not be changed, {@code false} if case should * be ignored and keyword-parts should be transformed to lower case. * @return a string array that contains the separated parts of the keywords, or null if no * keyword-parts have been found... */ public static String[] retrieveSeparatedKeywords(String[] keywords, boolean matchcase) { // when we receive an empty array, return null if (null == keywords || 0 == keywords.length) { return null; } // create new arraylist for retirn values List<String> separatedkws = new ArrayList<String>(); // first, add original keywords separatedkws.addAll(Arrays.asList(keywords)); // go through all found keywords for (String k : keywords) { // check, whether a keyword consists of more than one word, separated by , / - or +. // if so, split keyword at that char String[] sepkw = k.split("(,|/|-| |\\+)"); // and add eacxh single part to the linked list for (String sp : sepkw) { // trim leading and trailing spaces sp = sp.trim(); // if the user wants to ignore the case, transform string to lowercase if (!matchcase) { sp = sp.toLowerCase(); } // check whether the keyword-part has at least three chars and does // not equal typical words like "and", "or" etc... if (sp.length() > 2 && (!sp.equalsIgnoreCase("und") && !sp.equalsIgnoreCase("der") && !sp.equalsIgnoreCase("die") && !sp.equalsIgnoreCase("das") && !sp.equalsIgnoreCase("des") && !sp.equalsIgnoreCase("den") && !sp.equalsIgnoreCase("dem") && !sp.equalsIgnoreCase("von") && !sp.equalsIgnoreCase("als") && !sp.equalsIgnoreCase("aus") && !sp.equalsIgnoreCase("auf") && !sp.equalsIgnoreCase("ein") && !sp.equalsIgnoreCase("eine") && !sp.equalsIgnoreCase("einer") && !sp.equalsIgnoreCase("eines") && !sp.equalsIgnoreCase("oder") && !sp.equalsIgnoreCase("the") && !sp.equalsIgnoreCase("why") && !sp.equalsIgnoreCase("one") && !sp.equalsIgnoreCase("and"))) { separatedkws.add(sp); } } } // return linked list as array... return (separatedkws.size() > 0) ? separatedkws.toArray(new String[separatedkws.size()]) : null; } /** * This method "splits" a single <i>term</i> into its single word-parts. For example, a keyword * <i>Niklas, Luhmann - Zettelkasten</i> which is stored as a single keyword-entry, consists * of three parts (words). For creating the quick input keyword list (see CNewEntry), in some cases * we need the single parts of a keyword. * * @param settingsObj A reference to the CSettings-class * @param synonymsObj A reference to the CSynonyms-class * @param keyword the keyword, which should be split into its parts * @param matchcase {@code true} if the case should not be changed, {@code false} if case should * be ignored and keyword-parts should be transformed to lower case. * @return a string-array containing all splitted parts of the keywords, and spit-parts of the keyword- * related synonyms, if the keyword has any related synonyms. */ public static String[] getKeywordsAndSynonymsParts(Settings settingsObj, Synonyms synonymsObj, String keyword, boolean matchcase) { // first, get the separated parts from the selected value String[] kwparts = retrieveSeparatedKeywords(new String[] { keyword }, matchcase); // create array with keywords, and - if necessary - related synonyms String[] synline = null; // retrieve synonyms if option is set if (settingsObj.getSearchAlwaysSynonyms()) { synline = retrieveSeparatedKeywords(synonymsObj.getSynonymLine(keyword, matchcase), matchcase); } // if we don't have any synonyms, put only keywords in the array if (null == synline) { // we here copy our separated keywords to our find-array synline = kwparts; } else { // here we now have findterms in our array "textparts" and "findterms". // we need to put these arrays together. We do this by creating an array list // and adding all elements of both array to that list. Then we copy the final // array-list back to our findterms-array List<String> separatedkws = new ArrayList<String>(); // copy all elements of textparts-array to the array-list for (String part1 : kwparts) { if (!separatedkws.contains(part1)) { separatedkws.add(part1); } } // copy all elements of findterms-array to the array-list for (String part2 : synline) { if (!separatedkws.contains(part2)) { separatedkws.add(part2); } } // copy array-list to string-array synline = separatedkws.toArray(new String[separatedkws.size()]); } return synline; } /** * This method checks the string {@code expression} for occurences of regular-expression * meta-characters, that usually have to be escaped. * @param expression the string that should be checked for occurences of reg-ex-meta-characters * @return {@code true} if {@code expression} contains reg-ex-meta-chars, {@code false} otherwise */ public static boolean hasRegExChars(String expression) { // we assume having a regular expression only when we find certain meta-characters // to check for meta characters, we create an array with those chars and check whether // our searchterm "highlighterms" contains at least one of these chars... String[] allowedsigns = new String[] { "\\", "+", "{", "}", "(", ")", "[", "]", "$", "*", "?", ".", "|", "<", ">", "^" }; // init found-indicator boolean signfound = false; // iterate array for (String stdummy : allowedsigns) { if (expression.contains(stdummy)) { signfound = true; } } return signfound; } /** * This method converts all known markdown syntax into the application's "UBB"-format tags. * * @param dummy the entry content as string where Markdown should be converted to the common format tags * @return a string with "UBB"-format tags and no more Markdown syntax. Markdown has been replaced with * the common format tags of the Zettelkasten. */ public static String convertMarkDown2UBB(String dummy) { dummy = dummy.replace("[br]", "\n"); // quotes dummy = dummy.replaceAll("(^|\\n)(\\> )(.*)", "[q]$3[/q]"); // bullets dummy = dummy.replaceAll("(^|\\n)(\\d\\. )(.*)", "[n][*]$3[/*][/n]"); dummy = dummy.replace("[/n][n]", ""); dummy = dummy.replaceAll("(^|\\n)(\\* )(.*)", "[l][*]$3[/*][/l]"); dummy = dummy.replace("[/l][l]", ""); // bold and italic formatting in markdown dummy = dummy.replaceAll("\\*\\*\\*(.*?)\\*\\*\\*", "[f][k]$1[/k][/f]"); dummy = dummy.replaceAll("___(.*?)___", "[f][k]$1[/k][/f]"); // bold formatting dummy = dummy.replaceAll("__(.*?)__", "[f]$1[/f]"); dummy = dummy.replaceAll("\\*\\*(.*?)\\*\\*", "[f]$1[/f]"); // italic formatting dummy = dummy.replaceAll("_(.*?)_", "[k]$1[/k]"); dummy = dummy.replaceAll("\\*(.*?)\\*", "[k]$1[/k]"); // code blocks formatting dummy = dummy.replaceAll("\\`(.*?)\\`", "[code]$1[/code]"); // headlines dummy = dummy.replaceAll("(^|\\n)#{4} (.*)", "[h4]$2[/h4]"); dummy = dummy.replaceAll("(^|\\n)#{3} (.*)", "[h3]$2[/h3]"); dummy = dummy.replaceAll("(^|\\n)#{2} (.*)", "[h2]$2[/h2]"); dummy = dummy.replaceAll("(^|\\n)#{1} (.*)", "[h1]$2[/h1]"); // strike dummy = dummy.replaceAll("---(.*?)---", "[d]$1[/d]"); // images dummy = dummy.replaceAll("[!]{1}\\[([^\\[]+)\\]\\(([^\\)]+)\\)", "[img]$2[/img]"); dummy = dummy.replace("\n", "[br]"); return dummy; } public static String convertUBB2MarkDown(String dummy) { // bold formatting: [f] becomes <b> dummy = dummy.replaceAll("\\[f\\](.*?)\\[/f\\]", "**$1**"); // italic formatting: [k] becomes <i> dummy = dummy.replaceAll("\\[k\\](.*?)\\[/k\\]", "*$1*"); // headline: [h4] becomes <h5> dummy = dummy.replaceAll("\\[h4\\](.*?)\\[/h4\\]", "#### $1"); // headline: [h3] becomes <h4> dummy = dummy.replaceAll("\\[h3\\](.*?)\\[/h3\\]", "### $1"); // headline: [h2] becomes <h3> dummy = dummy.replaceAll("\\[h2\\](.*?)\\[/h2\\]", "## $1"); // headline: [h1] becomes <h2> dummy = dummy.replaceAll("\\[h1\\](.*?)\\[/h1\\]", "# $1"); // cite formatting: [q] becomes <q> dummy = dummy.replaceAll("\\[q\\](.*?)\\[/q\\]", "> $1"); // code formatting: [code] becomes ` dummy = dummy.replaceAll("\\[code\\](.*?)\\[/code\\]", "`$1`"); // strike-through formatting: [d] becomes <strike> dummy = dummy.replaceAll("\\[d\\](.*?)\\[/d\\]", "---$1---"); /* try { // unordered list: [l] becomes <ul> Pattern p = Pattern.compile("\\[l\\](.*?)\\[/l\\]"); // find all list-bullets inside Matcher m = p.matcher(dummy); while (m.find()) { String tmp = "[br]"+dummy.substring(m.start(), m.end()); tmp = tmp.replace("[*]", "* [br]"); dummy = dummy.substring(0, m.start())+tmp+dummy.substring(m.end()); } } catch (IndexOutOfBoundsException ex) { } */ dummy = dummy.replaceAll("\\[l\\](.*?)\\[/l\\]", "$1"); // ordered list: [n] becomes <ol> dummy = dummy.replaceAll("\\[n\\](.*?)\\[/n\\]", "$1"); // bullet points: [*] becomes <li> dummy = dummy.replaceAll("\\[\\*\\](.*?)\\[/\\*\\]", "* $1[br]"); // image dummy = dummy.replaceAll("\\[img\\](.*?)\\[/img\\]", "![Bild]($1)"); return dummy; } public static List<Integer> getImageResizeValues(String content) { // create image pattern Pattern p = Pattern.compile("\\[img\\]([^|]*)(.*?)\\[/img\\]"); // and matacher Matcher m = p.matcher(content); // init return value. This array contains all resize values of imagaes, if we have any List<Integer> resizevalues = new ArrayList<Integer>(); // find image tags while (m.find()) { // get group counts. usually, we only have two groups: // 1. the file path to the image file // 2. the resize value int maxcount = m.groupCount(); // get last group containing resize value // should start with "|", so remove it int reval; try { String resizeval = m.group(maxcount).substring(1); reval = Integer.parseInt(resizeval); resizevalues.add(reval); } catch (NumberFormatException ex) { } catch (IndexOutOfBoundsException ex) { } } return resizevalues; } /** * This method removes all UBB-format-tag from an entry and returns a "cleaned" string * of that entry's content that does no longer contain any UBB-Format-tags. * * @param content the entry's content that should be cleaned from UBB-format-tags * @param includeMarkdown if {@code true}, Markdown syntax will also be removed. If {@code false}, * Markdown syntax will remain in the string. This is needed when exporting content into Markdown * format and only not supported UBB tags should be removed, but not Markdown. * @return a cleaned string of that entry's content that does no longer contain any UBB-Format-tags */ public static String removeUbbFromString(String content, boolean includeMarkdown) { String dummy = ""; if (content != null && !content.isEmpty()) { dummy = content.replaceAll("\\[k\\]", "").replaceAll("\\[f\\]", "").replaceAll("\\[u\\]", "") .replaceAll("\\[h1\\]", "").replaceAll("\\[h2\\]", "").replaceAll("\\[h3\\]", "") .replaceAll("\\[h4\\]", "").replaceAll("\\[q\\]", "").replaceAll("\\[d\\]", "") .replaceAll("\\[c\\]", "").replaceAll("\\[code\\]", "").replaceAll("\\[sup\\]", "") .replaceAll("\\[sub\\]", "").replaceAll("\\[/k\\]", "").replaceAll("\\[/f\\]", "") .replaceAll("\\[/u\\]", "").replaceAll("\\[/h1\\]", "").replaceAll("\\[/h2\\]", "") .replaceAll("\\[/h3\\]", "").replaceAll("\\[/h4\\]", "").replaceAll("\\[/q\\]", "") .replaceAll("\\[/d\\]", "").replaceAll("\\[/c\\]", "").replaceAll("\\[/code\\]", "") .replaceAll("\\[/sup\\]", "").replaceAll("\\[/sub\\]", ""); // check whether only ubb or also markdown tags should be removed if (includeMarkdown) { // quotes dummy = dummy.replaceAll("(?<=\\[br\\])(\\> )(.*?)\\[br\\]", "$2[br]"); // bold and italic formatting in markdown dummy = dummy.replaceAll("\\*\\*\\*(.*?)\\*\\*\\*", "$1"); dummy = dummy.replaceAll("___(.*?)___", "$1"); // bold formatting dummy = dummy.replaceAll("__(.*?)__", "$1"); dummy = dummy.replaceAll("\\*\\*(.*?)\\*\\*", "$1"); // italic formatting dummy = dummy.replaceAll("_(.*?)_", "$1"); dummy = dummy.replaceAll("\\*(.*?)\\*", "$1"); // headlines dummy = dummy.replaceAll("#{4} (.*?)\\[br\\]", "$1"); dummy = dummy.replaceAll("#{3} (.*?)\\[br\\]", "$1"); dummy = dummy.replaceAll("#{2} (.*?)\\[br\\]", "$1"); dummy = dummy.replaceAll("#{1} (.*?)\\[br\\]", "$1"); // strike dummy = dummy.replaceAll("---(.*?)---", "$1"); // code dummy = dummy.replaceAll("\\`(.*?)\\`", "$1"); // images dummy = dummy.replaceAll("[!]{1}\\[([^\\[]+)\\]\\(([^\\)]+)\\)", ""); // hyperlinks dummy = dummy.replaceAll("\\[([^\\[]+)\\]\\(([^\\)]+)\\)", "$1 ($2)"); } dummy = dummy.replaceAll("\\[img\\](.*?)\\[/img\\]", ""); dummy = dummy.replaceAll("\\[fn ([^\\[]*)\\]", "[FN $1]"); dummy = dummy.replaceAll("\\[form ([^\\[]*)\\]", "$1"); dummy = dummy.replaceAll("\\[color ([^\\[]*)\\](.*?)\\[/color\\]", "$2"); dummy = dummy.replaceAll("\\[font ([^\\[]*)\\](.*?)\\[/font\\]", "$2"); dummy = dummy.replaceAll("\\[h ([^\\[]*)\\](.*?)\\[/h\\]", "$2"); dummy = dummy.replaceAll("\\[m ([^\\[]*)\\](.*?)\\[/m\\]", "$2"); dummy = dummy.replaceAll("\\[l\\](.*?)\\[/l\\]", "$1"); dummy = dummy.replaceAll("\\[\\*\\](.*?)\\[/\\*\\]", "- $1\n"); dummy = dummy.replace("[br]", System.getProperty("line.separator")); // convert tables. we don't do this with regular expressions // first, init the index-variable int pos = 0; int end; // go and find all table-tages while (pos != -1) { // find occurence of opening-tag pos = dummy.indexOf("[table]", pos); // when open-tag was found, go on and find end of table-tag if (pos != -1) { // find closing-tag end = dummy.indexOf("[/table]", pos); // if closing-tag also found, convert content to table if (end != -1) { try { // get table-content String tablecontent = dummy.substring(pos + 7, end).replaceAll("\\|", "\t") .replaceAll("\\^", "\t"); dummy = dummy.substring(0, pos) + tablecontent + dummy.substring(end + 8); pos = 0; } catch (IndexOutOfBoundsException ex) { } } // if no valid end-tag was found, try to find possible // next table tage else { pos = pos + 7; } } } } return dummy; } /** * Checks a given keyCode-value, usually passed from the {@code KeyEvent-getKeyCode()}-method, * and checks whether it is a "navigation" key like arrows, page up/down, home etc. * * @param keyCode the keycode of the pressed or releases key * @return {@code true} if it is a navigation key, false otherwise. */ public static boolean isNavigationKey(int keyCode) { return (KeyEvent.VK_HOME == keyCode || KeyEvent.VK_END == keyCode || KeyEvent.VK_UP == keyCode || KeyEvent.VK_DOWN == keyCode || KeyEvent.VK_LEFT == keyCode || KeyEvent.VK_RIGHT == keyCode); } /** * This method copies selected text in plain format into the clipboard. * * @param dataObj a reference to the CDaten class. Needed to retrieve the enty's content * @param displayedZettel the currently displayed entry * @param editorPane the editor pane which is the copy source */ public static void copyPlain(Daten dataObj, int displayedZettel, javax.swing.JEditorPane editorPane) { // create string builder that will contain complete plain entry StringBuilder plainEntry = new StringBuilder(""); // retrieve entry's title String title = dataObj.getZettelTitle(displayedZettel); // check whether entry has any title if (title != null && !title.isEmpty()) { // if yes, add title and line separator to string builder plainEntry.append(title); plainEntry.append(System.getProperty("line.separator")); } // retrieve plain entry that contains no ubb-tags and add it // to our string builder plainEntry.append(dataObj.getCleanZettelContent(displayedZettel)); // get start and end of selection int selstart = editorPane.getSelectionStart() - 1; int selend = editorPane.getSelectionEnd() - 1; // fix value if necessary if (selstart < 0) { selstart = 0; } // check whether end exceeds the string builders length - this is the case // e.g. if the user also selectes the time stamp... if (selend >= plainEntry.length()) { selend = plainEntry.length() - 1; } // copy selection to string String selectedText = plainEntry.toString().substring(selstart, selend).trim(); // if no text selected, quit if (selectedText.isEmpty()) { return; } // create new string-selection StringSelection stringSelection = new StringSelection(selectedText); // and copy string to clipboard Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); } /** * This method returns the current date as string in the following format: yymmddhhmm * (e.g. <i>0811271548</i> for the 27th Nov. 2008, 15:48 (3:48pm). Used to create * an entry's timestamp. * * @return the current date as string, for use as timestamp. */ public static String getTimeStamp() { // create new dateformat and format it in a simple way // so we just have the year (2 digits), the month and the day and the time. // e.g.: 0811271548 for the 27th Nov. 2008, 15:48 (3:48pm) DateFormat df = new SimpleDateFormat("yyMMddHHmm"); return df.format(new Date()); } /** * This method returns the current date as string in the following format: yymmddhhmmssms * (e.g. <i>081127154853157</i> for the 27th Nov. 2008, 15:48 (3:48pm) and 53 seconds and * 157 milliseconds. Used to create an entry's timestamp for the desktop-database. * * @return the current date as string, for use as timestamp. */ public static String getTimeStampWithMilliseconds() { // create new dateformat and format it in a simple way // so we just have the year (2 digits), the month and the day and the time. // e.g.: 0811271548 for the 27th Nov. 2008, 15:48 (3:48pm) DateFormat df = new SimpleDateFormat("yyMMddHHmmssSSS"); return df.format(new Date()); } /** * This method creates a unique ID for each entry. This ID is returned as string * and stored in the XML-structure. * * @param filename * @return a unique ID for an entry as String value */ public static String createZknID(String filename) { StringBuilder ts = new StringBuilder(getTimeStampWithMilliseconds()); ts.append(filename); int randomnumber = (int) (Math.random() * 99999); ts.append(String.valueOf(randomnumber)); return ts.toString(); } /** * This method accepts a single-line string {@code s} and returns it as line-separated * text, with each line having a maximum length of {@code len} chars.<br><br> * If the line wrapped text should have a prefix at the beginning of each line, * this can be passed as parameter {@code prefix}. * * @param s the single-lined string * @param len the length of each line from the returned string * @param prefix an optional string that is inserted infront of each line. * @return the string {@code s} with separated lines, with a line wrap after max. {@code len} chars. */ public static String lineWrapText(String s, int len, String prefix) { // create tokenizer StringTokenizer st = new StringTokenizer(s, " ", true); // dummy string String word; // string builder for return value StringBuilder sb = new StringBuilder((prefix != null && !prefix.isEmpty()) ? prefix + " " : ""); // line length counter int currentLineLen = 0; // iterate string while (st.hasMoreTokens()) { // check word-length int wordLen = (word = st.nextToken()).length(); // if we haven't reached the line end... if (currentLineLen + wordLen <= len) { // ...append word sb.append(word); // and increase line length counter currentLineLen += wordLen; } else { // else check whether first char in new line is a space char boolean firstIsSpace = word.charAt(0) == ' '; // append new line sb.append(System.getProperty("line.separator")); // append prefix, if we have any sb.append((prefix != null && !prefix.isEmpty()) ? prefix + " " : ""); // append word resp. nothing, if first char is space sb.append((firstIsSpace ? "" : word)); // reset line length counter currentLineLen = firstIsSpace ? 0 : wordLen; } } // return result return sb.toString(); } /** * This method converts separator chars of attachment- or image-paths into * the correct os-separator-char. This is needed, when the user uses the * program and data files both on windows or linux, for instance. * * @param csc the path to the attachment or image, as string. * @param settings a reference to the {@code CSettings} class. * @return the string {@code csc} with converted seperator chars, so the string * is usable for the current OS. */ public static String convertSeparatorChars(String csc, Settings settings) { // check for valid parameter if (null == csc || csc.isEmpty()) { return ""; } String retval = csc; // check whether attachment is a hyperlink or not. if it is *no* hyperlink, // we can convert the separator-chars - else we don't change them. if (!FileOperationsUtil.isHyperlink(csc)) { // check for os, and replace / with \ or \ with / (file separator-chars). // we do this since users can use their data on e.g. windows and linux, and therefor, // the separator-chars of images have to be switched. retval = (PlatformUtil.isWindows()) ? csc.replace("/", "\\") : csc.replace("\\", "/"); } return retval; } /** * This method either merges two synonym-lines from those synonyms that are associated with * the keywords {@code oldKw} and {@code newKw}. Or, if no synonyms are associated with the * new keyword {@code newKw}, the index-word from the old synonyms-line, which should equal the * value {@code oldKw}, is renamed to the new index-word {@code newKw}. * * @param synonymsObj * @param oldKw * @param newKw */ public static void mergeSynonyms(Synonyms synonymsObj, String oldKw, String newKw) { // check out, whether old and new keywords already exist as synonyms (index-words) int oldsynpos = synonymsObj.getSynonymPosition(oldKw); int newsynpos = synonymsObj.getSynonymPosition(newKw); // only proceed, if old keyword was an index-synonym... if (oldsynpos != -1) { // either the new keyword does not exist as synonym, than simply // ask to rename the old index-word into the new keyword if (-1 == newsynpos) { // create a JOptionPane with yes/no/cancel options int option = JOptionPane.showConfirmDialog(null, resourceMap.getString("renameIndexSynonymMsg"), resourceMap.getString("renameIndexSynonymTitle"), JOptionPane.YES_NO_OPTION, JOptionPane.PLAIN_MESSAGE); // proceed, if user wants to rename if (JOptionPane.YES_OPTION == option) { // rename index-word synonymsObj.setIndexWord(oldsynpos, newKw); // now ask the user whether he wants to keep the old keyword-name as additional synonym // create a JOptionPane with yes/no options option = JOptionPane.showConfirmDialog(null, resourceMap.getString("appendOldIndexSynonymMsg", newKw, oldKw, newKw), resourceMap.getString("appendOldIndexSynonymTitle"), JOptionPane.YES_NO_OPTION, JOptionPane.PLAIN_MESSAGE); // proceed, if user wants to append if (JOptionPane.YES_OPTION == option) { // append the former synonym, which was renamed, as additional synonym // to the new synonym-line with the new index-word synonymsObj.appendSingleSynonym(oldsynpos, oldKw); } } } // the new keyword is also a synonym... now offer to merge both the old // and the new synonyms-line else if (oldsynpos != newsynpos) { // create a JOptionPane with yes/no/cancel options int option = JOptionPane.showConfirmDialog(null, resourceMap.getString("mergeSynonymsMsg"), resourceMap.getString("mergeSynonymsTitle"), JOptionPane.YES_NO_OPTION, JOptionPane.PLAIN_MESSAGE); // proceed, if user wants to rename if (JOptionPane.YES_OPTION == option) { // rename index-word if (!synonymsObj.mergeSynonymLines(newsynpos, oldsynpos)) { // display error message box JOptionPane.showMessageDialog(null, resourceMap.getString("errorMergeSynonymsMsg"), resourceMap.getString("errorMergeSynonymsTitle"), JOptionPane.PLAIN_MESSAGE); } } } } } /** * */ public static void flushSessionLog() { Handler[] handlers = Constants.zknlogger.getHandlers(); for (Handler h : handlers) { if (h.toString().contains("StreamHandler")) { h.flush(); } } } /** * This method checks asciil-chars which were tread by an inputstream whether they are valid xml-chars * or not. * * @param zeichen the ascii-char we want to check * @return {@code true} if it is a valid char, {@code false} otherwise */ public static boolean isLegalJDOMChar(int zeichen) { // every character beginning from 0x20 is a legal xml-char // below 0x20 (space-char), the tab-char (0x09), carriage return (0x0A) and new line (0x0D) are valid onky return zeichen >= 32 || 9 == zeichen || 10 == zeichen || 13 == zeichen; } /** * This method checks whether a string contains legal JDOM chars, so the string can be added * as entry-content to the XML-data file. Every non-legal-JDOM-char is replaced by a space-char. * @param content the content-string that should be checked for valid JDOM-chars. typically use the input * from a user made in the CNewEntry-dialog. * @return a "cleaned" string without any illegal JDOM-chars. Illegal chars are replaced by space-chars */ public static String isValidJDOMChars(String content) { // copy content to char-array char[] contentchars = content.toCharArray(); // create return value StringBuilder retval = new StringBuilder(""); // iterate char-array and check for valid JDOM chars for (char c : contentchars) { // is char legal JDOM-char? then append it to string builder else append space char retval.append((isLegalJDOMChar(c)) ? c : " "); } // return cleaned string return retval.toString(); } /** * This method extracts all occurences of possible form-tags (Laws of Form) * from an entry-string, which content is stored in {@code content}. The found * form-tags will be returned as an array list of strings. * * @param content a string with zettel content, which may contain form-tags * @return an array list with all form-tags of that entry as strings, or {@code null} * if no form tag was found. */ public static ArrayList<String> getFormsFromString(String content) { // check for valid param if (null == content || content.isEmpty()) { return null; } // create new array list ArrayList<String> forms = new ArrayList<String>(); // find forms. we don't do this with regular expressions // first, init the index-variable int pos = 0; int end; // go and find all form-tages while (pos != -1) { // find occurence of opening-tag pos = content.indexOf(Constants.FORMAT_FORM_TAG, pos); // when open-tag was found, go on and find end of table-tag if (pos != -1) { // find closing-tag end = content.indexOf("]", pos); // if closing-tag also found, add form to arraylist if (end != -1) { try { forms.add(content.substring(pos, end + 1)); } catch (IndexOutOfBoundsException ex) { } } pos = end; } } return forms; } /** * This method returns the XML database as string. just for testing purposes. * @param dataObj * @return */ public static String retrieveXMLFileAsString(Daten dataObj) { // create a new XML-outputter with the pretty output format, // so the xml-file looks nicer XMLOutputter out = new XMLOutputter(Format.getPrettyFormat()); // return XML data base as string return out.outputString(dataObj.getZknData()); } public static boolean isPandocExportType(int exportType) { // check if export to pandoc format is requested return (Constants.EXP_TYPE_DESKTOP_DOCX == exportType || Constants.EXP_TYPE_DESKTOP_ODT == exportType || Constants.EXP_TYPE_DESKTOP_EPUB == exportType || Constants.EXP_TYPE_DESKTOP_RTF == exportType || Constants.EXP_TYPE_DOCX == exportType || Constants.EXP_TYPE_ODT == exportType || Constants.EXP_TYPE_EPUB == exportType || Constants.EXP_TYPE_RTF == exportType); } public static boolean isPandocMissing(Settings settings, int exportType) { // start pandoc for conversion Runtime rt = Runtime.getRuntime(); Process pr = null; boolean pandocmissing = false; // check if export to pandoc format is requested if (isPandocExportType(exportType)) { try { pr = rt.exec(settings.getPandocPath() + " --version"); pr.waitFor(); } catch (IOException ex) { Constants.zknlogger.log(Level.WARNING, "Could not find Pandoc under specified path {0}.", settings.getPandocPath()); pandocmissing = true; } catch (InterruptedException ex) { Constants.zknlogger.log(Level.WARNING, "Could not find Pandoc under specified path {0}.", settings.getPandocPath()); pandocmissing = true; } // destroy process if (pr != null) { Constants.zknlogger.log(Level.INFO, "Process exit code: {0}", String.valueOf(pr.exitValue())); pr.destroy(); } // check whether pandoc is available if (pandocmissing) { // if not, show error message and leave JOptionPane.showMessageDialog(null, resourceMap.getString("noPandocInstalledMsg"), resourceMap.getString("noPandocInstalledTitle"), JOptionPane.PLAIN_MESSAGE); } } return pandocmissing; } public static LinkedList extractFootnotesFromContent(String content) { // now prepare a reference list from possible footnotes LinkedList<String> footnotes = new LinkedList<String>(); // position index for finding the footnotes int pos = 0; // do search as long as pos is not -1 (not-found) while (pos != -1) { // find the html-tag for the footnote pos = content.indexOf(Constants.footnoteHtmlTag, pos); // if we found something... if (pos != -1) { // find the closing quotes int end = content.indexOf("\"", pos + Constants.footnoteHtmlTag.length()); // if we found that as well... if (end != -1) { // extract footnote-number String fn = content.substring(pos + Constants.footnoteHtmlTag.length(), end); // and add it to the linked list, if it doesn't already exist if (-1 == footnotes.indexOf(fn)) footnotes.add(fn); // set pos to new position pos = end; } else pos = pos + Constants.footnoteHtmlTag.length(); } } return footnotes; } public static AbstractButton makeTexturedToolBarButton(AbstractButton button, String segmentPosition) { if (null == segmentPosition || segmentPosition.isEmpty() || segmentPosition.equals(SEGMENT_POSITION_ONLY)) { button.putClientProperty("JButton.buttonType", "textured"); } else { button.putClientProperty("JButton.buttonType", "segmentedTextured"); button.putClientProperty("JButton.segmentPosition", segmentPosition); } button.setText(null); button.setBorderPainted(true); button.setPreferredSize(Constants.seaGlassButtonDimension); return button; } }