package net.sourceforge.cardme.util; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.cardme.io.BinaryFoldingScheme; import net.sourceforge.cardme.io.FoldingScheme; /* * Copyright 2011 George El-Haddad. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY GEORGE EL-HADDAD ''AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GEORGE EL-HADDAD OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of George El-Haddad. */ /** * * @author George El-Haddad * <br/> * Sep 21, 2006 * * <p>Utility methods to help with VCard parsing and reading.</p> */ public final class VCardUtils { /** * <p>Carriage Return character. CR</p> */ public static final String CR = "\r"; /** * <p>Line Feed character. LF</p> */ public static final String LF = "\n"; /** * <p>Horizontal Tab character. HT</p> */ public static final String HT = "\t"; /** * <p>Space character. SP</p> */ public static final String SP = " "; /** * <p>Internet Standard "END OF LINE" mark. CRLF</p> */ public static final String CRLF = CR + LF; /** * <p>The "END OF LINE" delimiter used for folding labels</p> */ public static final String LABEL_DELIMETER = "="; /** * <p>List of characters that need escaping.</p> */ private static final char[] NEED_ESCAPING = new char[] { ',', ';', ':', '\\', '\n', '\r' }; /** * <p>List of characters that need un-escaping.</p> */ private static final String[] NEED_UNESCAPING = new String[] { "\\r", "\\n", "\\N", "\\\\", "\\,", "\\;", "\\:" }; private static NumberFormat GEO_NUM_FORMATTER = NumberFormat.getNumberInstance(); static { GEO_NUM_FORMATTER.setMinimumFractionDigits(6); GEO_NUM_FORMATTER.setMaximumFractionDigits(6); GEO_NUM_FORMATTER.setMaximumIntegerDigits(6); GEO_NUM_FORMATTER.setMinimumIntegerDigits(1); GEO_NUM_FORMATTER.setGroupingUsed(false); } /** * <p>Private constructor.</p> */ private VCardUtils() { } /** * <p>Returns true if the specified textual string contains any * one of the following characters defined in the constant char[] * of {@link #NEED_ESCAPING}.</p> * * @param text * @return boolean */ public static boolean needsEscaping(String text) { boolean needs = false; for(int i = 0; i < NEED_ESCAPING.length; i++) { if(text.indexOf(NEED_ESCAPING[i]) != -1) { needs = true; break; } } return needs; } /** * <p>Returns true if the specified textual string contains any * one of the following characters defined in the constant String[] * of {@link #NEED_UNESCAPING}.</p> * * @param text * @return boolean */ public static boolean needsUnEscaping(String text) { boolean needs = false; for(int i = 0; i < NEED_UNESCAPING.length; i++) { if(text.indexOf(NEED_UNESCAPING[i]) != -1) { needs = true; break; } } return needs; } /** * <p>Effectively un-escapes a string, performs the reverse of {@link #escapeString(String)}.</p> * * @param text * @return {@link String} */ public static String unescapeString(String text) { if (!needsUnEscaping(text)){ return text; } String unescaped = text.replaceAll("\\\\n", "\n"); unescaped = unescaped.replaceAll("\\\\r", "\r"); unescaped = unescaped.replaceAll("\\\\N", "\n"); unescaped = unescaped.replaceAll("\\\\\\\\", "\\\\"); unescaped = unescaped.replaceAll("\\\\,", ","); unescaped = unescaped.replaceAll("\\\\;", ";"); unescaped = unescaped.replaceAll("\\\\:", ":"); return unescaped; } /** * <p>Escapes special characters in a string to be suitable for SQL. * Characters that are escaped are the following: * <ul> * <li><b>EOL</b> -> \n or \N</li> * <li><b>\</b> -> \\</li> * <li><b>,</b> -> \,</li> * <li><b>;</b> -> \;</li> * </p> * * @param text * @return {@link String} */ public static String escapeString(String text) { if(!needsEscaping(text)) { return text; } StringBuilder sb = new StringBuilder(text.length()); for(int i = 0; i < text.length(); ++i) { char c = text.charAt(i); switch (c) { case '\n': { sb.append('\\'); sb.append('n'); break; } case '\r': { sb.append('\\'); sb.append('r'); break; } case '\\': { sb.append('\\'); sb.append('\\'); break; } case ',': { sb.append('\\'); sb.append(','); break; } case ';': { sb.append('\\'); sb.append(';'); break; } case ':': { sb.append('\\'); sb.append(':'); break; } default: { sb.append(c); } } } return sb.toString(); } /** * <p>Returns true if the given String <code>line</code> is greater than the maximum allowed * characters needed for folding (75 chars per line excluding CRLF.)</p> * * @param line * @return boolean */ public static boolean needsFolding(String line) { return needsFolding(line, FoldingScheme.MIME_DIR); } /** * <p>Returns true if the given String is greater than the maximum allowed * characters needed for folding as specified by <code>foldingScheme</code> excluding * the CRLF at the end.</p> * * @param line * @param foldingScheme * @return boolean */ public static boolean needsFolding(String line, FoldingScheme foldingScheme) { return needsFolding(line, foldingScheme.getMaxChars()); } /** * <p>Returns true if the given String is greater than the maximum allowed * characters needed for folding as specified by <code>foldingScheme</code> excluding * the CRLF at the end.</p> * * @param line * @param maxChars * @return boolean */ public static boolean needsFolding(String line, int maxChars) { if(maxChars < 0) { return false; } else { return line.length() > maxChars; } } /** * <p>First line unfolds all the labels * Second line unfolds all the lines (if needed.)</p> * * <p>This unfolding technique is according to the MIME-DIR specifications * as described in section 5.8.1 "Line delimiting and folding."</p> * * @param vcardString * @return {@link String} */ public static String unfoldVCard(String vcardString) { //Reformat quoted printable soft line break that violates RFC-822 (if present) //Unfolds quoted-printable lines that look like this: // //LABEL;HOME;ENCODING=QUOTED-PRINTABLE:Silicon Alley 5,=0D=0A= //New York, New York 12345=0D=0A= //USA // //1. Each line (except for the last line) ends in a "=", which should be removed //2. The 2nd and 3rd lines should be indented with whitespace, but they are not //This behavior is exhibited by vCards generated by Outlook (http://sourceforge.net/tracker/?func=detail&aid=3496318&group_id=99914&atid=625723) Pattern unfoldOutlookCraziness = Pattern.compile("^([^:]*QUOTED-PRINTABLE.*?:.*?)=\n\\s*((.*?=\n\\s*)*)(.*?(\n|$))", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); String unfold1 = "=\n(\\p{Blank}*(=))"; String unfold2 = "=\n([^\\p{Blank}]([^:\n]*))\n"; String unfold3 = "\\p{Blank}+="; String unfold4 = "\n\\p{Blank}+"; StringBuffer sb = new StringBuffer(); Matcher m = unfoldOutlookCraziness.matcher(vcardString); while (m.find()){ StringBuilder sb2 = new StringBuilder(m.group(1)); if (m.group(2) != null){ //remove equal sign, new line, and whitespace (in case the line is actually properly folded) sb2.append(m.group(2).replaceAll("=\\n\\s*", "")); } sb2.append(m.group(4)); m.appendReplacement(sb, sb2.toString()); } m.appendTail(sb); String s0 = sb.toString(); String s1 = s0.replaceAll(unfold1, "$1"); String s2 = s1.replaceAll(unfold2, "$1"); String s3 = s2.replaceAll(unfold4, ""); String s4 = s3.replaceAll(unfold3, "="); return s4; } /** * <p>Takes a single line and folds it according to the MIME-DIR specification. * A line is folded its length exceeds 75 characters. For every 75 * characters a CRLF is appended to the end, the following line will begin * with a SP character and then followed by the usual next 75 chars + CRLF * until the end.</p> * * <p>This method will not check the length of the String before folding, use * the VCardUtils.needsFolding(String) method before invoking this one. This * method will start the folding procedure right away. Running a line that * doesn't need folding through here won't affect the outcome, but creates * extra over head.</p> * * @param thisLine * @return {@link String} */ public static String foldLine(String thisLine) { return foldLine(thisLine, FoldingScheme.MIME_DIR); } /** * <p>Takes a single line and folds it according to the MIME-DIR specification. * The line is folded if its length exceeds <code>maxChars</code> characters. * For every <code>maxChars</code> characters a CRLF is appended to the end, * the following line will begin with a SP character and then followed by the * usual next <code>maxChars</code> chars + CRLF until the end.</p> * * <p>Strings coming out of here will always terminate with CRLF.</p> * * <p>This method will not check the length of the String before folding, use * the VCardUtils.needsFolding(String) method before invoking this one. This * method will start the folding procedure right away. Running a line that * doesn't need folding through here won't affect the outcome, but creates * extra over head.</p> * * @param thisLine * @param foldingScheme * @return {@link String} */ public static String foldLine(String thisLine, FoldingScheme foldingScheme) { return foldLine(thisLine, VCardUtils.CRLF, foldingScheme); } /** * <p>Takes a single line and folds it according to the MIME-DIR specification. * The line is folded if its length exceeds <code>maxChars</code> characters. * For every <code>maxChars</code> characters a CRLF is appended to the end, * the following line will begin with a SP character and then followed by the * usual next <code>maxChars</code> chars + CRLF until the end.</p> * * <p>Strings coming out of here will always terminate with CRLF.</p> * * <p>This method will not check the length of the String before folding, use * the VCardUtils.needsFolding(String) method before invoking this one. This * method will start the folding procedure right away. Running a line that * doesn't need folding through here won't affect the outcome, but creates * extra over head.</p> * * @param thisLine * @param binaryFoldingScheme * @return {@link String} */ public static String foldLine(String thisLine, BinaryFoldingScheme binaryFoldingScheme) { return foldLine(thisLine, VCardUtils.CRLF, binaryFoldingScheme.getMaxChars(), binaryFoldingScheme.getIndent()); } /** * <p>Does the same job as {@link #foldLine(String)} but allows you * to specify a String to be append when the line folds. By default * it is CRLF. Here you can specify what you want. Useful when folding * Quoted-Printable labels. Though a trailing delimiter may appear.</p> * * @param thisLine * @param eolDelimeter * @param binaryFoldingScheme * @return {@link String} */ public static String foldLine(String thisLine, String eolDelimeter, BinaryFoldingScheme binaryFoldingScheme) { return foldLine(thisLine, eolDelimeter, binaryFoldingScheme.getMaxChars(), binaryFoldingScheme.getIndent()); } /** * <p>Does the same job as {@link #foldLine(String)} but allows you * to specify a String to be append when the line folds. By default * it is CRLF. Here you can specify what you want. Useful when folding * Quoted-Printable labels. Though a trailing delimiter may appear.</p> * * @param thisLine * @param eolDelimeter * @param foldingScheme * @return {@link String} */ public static String foldLine(String thisLine, String eolDelimeter, FoldingScheme foldingScheme) { return foldLine(thisLine, eolDelimeter, foldingScheme.getMaxChars(),foldingScheme.getIndent()); } /** * <p>Does the same job as {@link #foldLine(String)} but allows you * to specify a String to be append when the line folds. By default * it is CRLF. Here you can specify what you want. Useful when folding * Quoted-Printable labels. Though a trailing delimiter may appear.</p> * * @param thisLine * @param eolDelimeter * @param maxChars * @param indent * @return {@link String} */ public static String foldLine(String thisLine, String eolDelimeter, int maxChars, String indent) { if(!needsFolding(thisLine, maxChars)) { return thisLine; } boolean loop = true; boolean first = true; int crnt = 0; int prev = 0; StringBuilder builder = new StringBuilder(); while (loop) { prev = crnt; crnt = crnt + maxChars; if (crnt > thisLine.length()) { // Append any extra characters at the end if (prev < thisLine.length()) { if (!first) { builder.append(indent); // on some rare occasions } builder.append(thisLine.substring(prev).trim()); if(eolDelimeter != null) builder.append(eolDelimeter); } loop = false; } else { if (!first) { builder.append(indent); } else { first = false; } builder.append(thisLine.substring(prev, crnt).trim()); if(eolDelimeter != null) { builder.append(eolDelimeter); } } } String finalStr = builder.toString(); if(finalStr.endsWith(eolDelimeter)) { finalStr = finalStr.substring(0, finalStr.lastIndexOf(eolDelimeter)); } return finalStr.trim(); // removes any extra white-space at the end } /** * <p>Returns the number formatter for the geographical position feature. * This will format the floating value to the 6th decimal place.</p> * * @return {@link NumberFormat} */ public static NumberFormat getGeographicPositionFormatter() { return GEO_NUM_FORMATTER; } /** * <p>Parses the specified {@link String} <code>str</code> with the given * delimiter <code>delim</code> and returns an array of {@link String} * delimited by the said delimiter. Note that this method takes into * account characters escaped by a backslash.</p> * * @param str * @param delim * @return {@link String}[] */ public static String[] parseStringWithEscappedDelimiter(String str, char delim) { int i = 0; int state = 0; char c; int prev = 0; boolean firstTime = true; char[] testStringarr = str.toCharArray(); List<String> strList = new ArrayList<String>(5); for(int len = testStringarr.length; i < len; i++) { c = testStringarr[i]; if(state == 0) { if(c == '\\') { state++; } else if (c == delim) { if(firstTime) { strList.add(str.substring(prev, i)); } else { strList.add(str.substring(prev+1, i)); } if(prev == 0) { firstTime = false; } prev = i; } } else { state--; } } if(firstTime) { strList.add(str.substring(prev)); } else { strList.add(str.substring(prev+1)); } return strList.toArray(new String[strList.size()]); } }