import java.io.*; import java.util.*; import java.text.*; import java.nio.charset.*; class VCard implements Comparable<VCard> { static String[] ttel = new String[] { "CELL", "HOME", "WORK" }; static String[] tadr = new String[] { "HOME", "WORK" }; static String[] imgf = new String[] { "JPEG", "PNG", "GIF" }; Charset charset; int specify; String fname = null, org = null, title = null, note = null; String[] name; byte[] photo = null; int itphoto = 0; Object obj; final List<Itm> tel = new ArrayList<>(), adr = new ArrayList<>(), email = new ArrayList<>(), url = new ArrayList<>(); int ptel = -1, padr = -1, pemail = -1, purl = -1; Date bday = null, rev = null; @SuppressWarnings({ "rawtypes", "unchecked" }) final List<Itm>[] al = (List<Itm>[]) new List[] { tel, adr, email, url }; //VCard() { } @Override public int compareTo(VCard vc) { String s1 = (fname == null ? "" : fname), s2 = (vc.fname == null ? "" : vc.fname); return s1.compareToIgnoreCase(s2); } @Override public String toString() { String st = "VCard" + "\n"; if (fname != null) st += "FN: " + fname + "\n"; if (name != null) st += "N[]: " + String.join(",", name) + "\n"; if (bday != null) st += "BDAY: " + DateFormat.getDateInstance(DateFormat.SHORT).format(bday) + "\n"; if (org != null) st += "ORG: " + org + "\n"; if (title != null) st += "TITLE: " + title + "\n"; if (photo != null) st += "PHOTO: " + imgf[itphoto < 0 || itphoto >= imgf.length ? 0 : itphoto] + " " + photo.length + " Bt\n"; for (List<Itm> litm : al) for (int i = 0; i < litm.size(); i++) if (litm.get(i) != null) { if (litm == tel) st += "TEL: " + (String)tel.get(i).value + " " + ttel[tel.get(i).idl < 0 || tel.get(i).idl >= ttel.length ? 0 : tel.get(i).idl] + (ptel == i ? " PREF" : ""); else if (litm == adr) st += "ADR[]: " + String.join(",", (String[])adr.get(i).value).replace('\n', ' ') + " " + tadr[adr.get(i).idl < 0 || adr.get(i).idl >= tadr.length ? 0 : adr.get(i).idl] + (padr == i ? " PREF" : ""); else if (litm == email) st += "EMAIL: " + (String)email.get(i).value + (pemail == i ? " PREF" : ""); else if (litm == url) st += "URL: " + (String)url.get(i).value + (purl == i ? " PREF" : ""); st += " " + litm.get(i).note.replace('\n', ' ') + "\n"; } if (note != null) st += "NOTE: " + note.replace('\n', ' ') + "\n"; if (rev != null) st += "REV: " + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM).format(rev) + "\n"; return st; } boolean contains(String st) { st = st.toUpperCase(); if ((fname != null && fname.toUpperCase().indexOf(st) > -1) || (org != null && org.toUpperCase().indexOf(st) > -1) || (title != null && title.toUpperCase().indexOf(st) > -1) || (note != null && note.toUpperCase().indexOf(st) > -1) || (bday != null && DateFormat.getDateInstance(DateFormat.SHORT).format(bday).indexOf(st) > -1) || (rev != null && DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM).format(rev).indexOf(st) > -1)) return true; if (name != null) for (String s: name) if (s != null && s.toUpperCase().indexOf(st) > -1) return true; String stt = ctel(st); for (List<Itm> litm : al) for (Itm itm : litm) if (itm != null) { if (itm.note != null && itm.note.toUpperCase().indexOf(st) > -1) return true; if (itm.value != null) { if (litm == adr) for (String s : (String[])itm.value) { if (s != null && s.toUpperCase().indexOf(st) > -1) return true; } else if (((String)itm.value).toUpperCase().indexOf(st) > -1) return true; } if (litm == tel && !stt.isEmpty()) if (ctel((String)itm.value).indexOf(stt) > -1) return true; } return false; } String ctel(String st) { StringBuilder sb = new StringBuilder(); char ch; for (int i = 0; i < st.length(); i++) { ch = st.charAt(i); if ((ch >= '0' && ch <= '9') || ch == '+') sb.append(ch); } return sb.toString(); } static BufferedInputStream bis; static int ch; static List<VCard> read(InputStream in, Charset cs) throws IOException { VCard.bis = new BufferedInputStream(in, 1024); Map<String, String> mNote = new HashMap<>(); int b, e, ind; boolean b64, qp, pref; int itel, iadr, iimgf; byte[] bt; Charset scs; String st, mc, m; if (!"BEGIN:VCARD".equals(new String(bis.readNBytes(11), StandardCharsets.ISO_8859_1))) throw new IOException("Format error"); if (cs == null) cs = Charset.defaultCharset(); List<VCard> lst = new ArrayList<>(); VCard vc = new VCard(); ch = -1; do { st = readk(); if (st == null) continue; b64 = qp = pref = false; itel = iadr = iimgf = -1; scs = null; bt = null; m = ""; b = 0; mc = null; do { e = st.indexOf(';', b); String prm = st.substring(b, (e < 0 ? st.length() : e)).strip(); if (b == 0) { ind = prm.lastIndexOf('.'); if (ind >= 0) { m = prm.substring(0, ind); prm = prm.substring(ind + 1); } mc = prm; } else { if (!qp && prm.endsWith("QUOTED-PRINTABLE")) qp = true; if (!b64 && prm.endsWith("BASE64")) b64 = true; if (itel == -1) itel = arrs(ttel, prm); if (iadr == -1) iadr = arrs(tadr, prm); if (iimgf == -1) iimgf = arrs(imgf, prm); if (scs == null && prm.startsWith("CHARSET=")) { try { scs = Charset.forName(prm.substring(8)); } catch (Exception ex) { } } if (!pref && prm.indexOf("PREF") >= 0) pref = true; } b = e + 1; } while (e >= 0); if ("END".equals(mc) && "VCARD".equals(new String(readv(), StandardCharsets.ISO_8859_1))) { for (List<Itm> litm : vc.al) for (Itm itm : litm) if (!itm.m.isEmpty()) { String s = mNote.get(itm.m); if (s != null) itm.note = s; }; lst.add(vc); mNote.clear(); vc = new VCard(); continue; } if (scs == null) scs = cs; if (b64) { bt = readb64(); } else if (qp) { bt = readqp(); } else { bt = readv(); } if ("FN".equals(mc)) vc.fname = new String(bt, scs); else if ("N".equals(mc)) vc.name = arrf(bt, 5, scs); else if ("ORG".equals(mc)) vc.org = new String(bt, scs); else if ("TITLE".equals(mc)) vc.title = new String(bt, scs); else if ("BDAY".equals(mc)) vc.bday = parseDate(new String(bt, StandardCharsets.ISO_8859_1)); else if ("REV".equals(mc)) vc.rev = parseDate(new String(bt, StandardCharsets.ISO_8859_1)); else if ("NOTE".equals(mc)) { if (m.isEmpty()) vc.note = new String(bt, scs); else { mNote.put(m, new String(bt, scs)); } } else if ("PHOTO".equals(mc)) { vc.photo = bt; vc.itphoto = (iimgf < 0 ? 0 : iimgf); } else if ("TEL".equals(mc)) { vc.tel.add(new Itm(m, (itel < 0 ? 0 : itel), new String(bt, scs) ,"")); if (pref) vc.ptel = vc.tel.size() - 1; } else if ("EMAIL".equals(mc)) { vc.email.add(new Itm(m, -1, new String(bt, scs), "")); if (pref) vc.pemail = vc.email.size() - 1; } else if ("URL".equals(mc)) { vc.url.add(new Itm(m, -1, new String(bt, scs), "")); if (pref) vc.purl = vc.url.size() - 1; } else if("ADR".equals(mc)) { vc.adr.add(new Itm(m, (iadr < 0 ? 0 : iadr), arrf(bt, 7, scs), "")); if (pref) vc.padr = vc.adr.size() - 1; } } while (ch != -1); return lst; } static int arrs(String arr[], String st) { for (int i = 0; i < arr.length; i++) if (st.endsWith(arr[i])) return i; return -1; } static String[] arrf(byte[] bt, int sz, Charset cs) { String[] ms = new String[sz]; ByteArrayOutputStream baos = new ByteArrayOutputStream(); boolean b = false; int c = 0; for (int i = 0; i < bt.length; i++) { if (!b && bt[i] == '\\') { b = true; continue; } if (b) b = false; else if (bt[i] == '\\') continue; else if (bt[i] == ';') { if (c >= ms.length - 1) break; ms[c++] = baos.toString(cs); baos.reset(); continue; } baos.write(bt[i]); } ms[c] = baos.toString(cs); return ms; } static String readk() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); while((ch = bis.read()) != -1) { if (ch == '\n') return null; if (ch == ':') break; if (ch > 0x20) baos.write((byte) ch); } return (baos.size() == 0 ? null : baos.toString(StandardCharsets.ISO_8859_1).toUpperCase()); } static byte[] readv() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); while((ch = bis.read()) != -1) { if (ch == '\n') { bis.mark(10); if ((ch = bis.read()) == -1) break; if (ch == 0x20) continue; bis.reset(); break; } if (ch != '\r') baos.write((byte) ch); } return baos.toByteArray(); } static byte[] readb64() throws IOException { byte[] bt = readv(); try { return Base64.getDecoder().decode(bt); } catch (Exception ex) { return null; } } static byte[] readqp() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int c = 0, u = -1, l; while((ch = bis.read()) != -1) { if (ch >= 0x20 || ch == '\n' || ch == '\t') { if (ch == '=') { c = 1; continue; } if (c == 0 && ch == '\n') break; if (c == 1 && ch == '\n') { c = 0; continue; } if (c == 1) { c = 2; u = Character.digit((char) ch, 16); continue; } if (c == 2) { c = 0; l = Character.digit((char) ch, 16); if (u == -1 || l == -1) continue; ch = ((u << 4) + l); if (ch < 0x20 && ch != '\n' && ch != '\t') continue; } baos.write((byte) ch); } } return baos.toByteArray(); } static Date parseDate(String source) { source = source.replace("-", "").replace(":", "").toUpperCase(); String pattern; if (source.length() > 14) { source = source.substring(0, 15); pattern = "yyyyMMdd'T'HHmmss"; } else pattern = "yyyyMMdd"; try { return new SimpleDateFormat(pattern).parse(source); } catch (Exception e) { return null; } } final int size = 75; OutputStream out; static void write(OutputStream out, List<VCard> lst, Charset charset, int specify) throws IOException { for (VCard vc : lst) vc.write(out, charset, specify); } void write(OutputStream out, Charset charset, int specify) throws IOException { this.out = out; this.charset = charset; this.specify = specify; String dptrn1 = "yyyyMMdd"; String dptrn2 = "yyyyMMdd'T'HHmmss'Z'"; int n = 1; String s, mrk = "M"; boolean b; appln("BEGIN:VCARD"); appln("VERSION:2.1"); app("FN", fname, true); app("N", name, 5, true); if (bday != null) appln("BDAY:" + new SimpleDateFormat(dptrn1).format(bday)); app("ORG", org, false); app("TITLE", title, false); for (List<Itm> litm : al) for (int i = 0; i < litm.size(); i++) { s = ((b = !litm.get(i).note.isEmpty()) ? mrk + (n++) + "." : ""); if (litm == tel) app(al(s + "TEL", ttel, tel.get(i).idl, (i == ptel)), (String)tel.get(i).value, b); else if (litm == email) app(al(s + "EMAIL", null, 0, (i == pemail)), (String)email.get(i).value, b); else if (litm == url) app(al(s + "URL", null, 0, (i == purl)), (String)url.get(i).value, b); else if (litm == adr) app(al(s + "ADR", tadr, adr.get(i).idl, (i == padr)), (String[])adr.get(i).value, 7, b); if (b) app(s + "NOTE", litm.get(i).note, false); } if (photo != null && photo.length != 0) { String st = "PHOTO;TYPE="+ imgf[itphoto < 0 || itphoto >= imgf.length ? 0 : itphoto] +";ENCODING=BASE64:" + Base64.getEncoder().encodeToString(photo); appln(st.substring(0, Math.min(st.length(), size + 1))); for (int index = size + 1; index < st.length(); index += size) appln(" " + st.substring(index, Math.min(st.length(), index + size))); out.write('\r'); out.write('\n'); } app("NOTE", note, false); if (rev != null) appln("REV:" + new SimpleDateFormat(dptrn2).format(rev)); appln("END:VCARD"); } String al(String st, String[] ms, int i, boolean b) { StringBuilder sb = new StringBuilder(st); if (ms != null) sb.append(';').append(ms[(i < 0 || i >= ms.length ? 0 : i)]); if (b) sb.append(";PREF"); return sb.toString(); } void appln(String st) throws IOException { out.write(st.getBytes(StandardCharsets.ISO_8859_1)); out.write('\r'); out.write('\n'); } void app(String st, String s, boolean required) throws IOException { app(st, new String[] { s }, 1, required); } void app(String st, String[] ms, int len, boolean required) throws IOException { boolean ascii = true, empty = true, singleline = true; StringBuilder sb = new StringBuilder(); for (int j = 0; j < len; j++) { if (j > 0) sb.append(';'); String s = (ms != null && j < ms.length && ms[j] != null ? ms[j] : ""); int end = s.length() - 1; while (end >= 0 && s.charAt(end) == 0x20) end--; for (int i = 0; i <= end; i++) { char ch = s.charAt(i); if (ch >= 0x20 || ch == '\n' || ch == '\t') { if (len > 1 && (ch == '\\' || ch ==';')) sb.append('\\'); if (ascii && ch > 0x7e) ascii = false; if (singleline && ch == '\n') singleline = false; if (empty) empty = false; sb.append(ch); } } } if (required || !empty) { byte[] bt = sb.toString().getBytes(charset); sb.setLength(0); int spc = specify; if (ascii) spc = 2; if (!singleline) spc = 0; sb.append(st); if (spc < 2) sb.append(";CHARSET=").append(charset.name().toUpperCase()); if (spc < 1) sb.append(";ENCODING=QUOTED-PRINTABLE"); sb.append(':'); if (spc < 1) { // 0-Charset+QP, 1-Specified, 2-Not specified for (int cnt = sb.length(), i = 0; i < bt.length; i++) { int ch = (bt[i] & 0xFF); if (cnt >= size) { sb.append("=\r\n"); cnt = 0; } if (ch == '\n') { sb.append("=0D=0A"); cnt = 0; if (bt.length - i > 1) sb.append("=\r\n"); } else if (ch >= 0x20 && ch <= 0x7e && ch != '=') { sb.append((char)ch); cnt++; } else { sb.append(String.format("=%02X", ch)); cnt += 3; } } appln(sb.toString()); } else { out.write(sb.toString().getBytes(StandardCharsets.ISO_8859_1)); out.write(bt); out.write('\r'); out.write('\n'); } } } static class Itm { int idl; String m, note; Object value; Itm(String m, int idl, Object value, String note) { this.m = m; this.idl = idl; this.value = value; this.note = note; } @Override public String toString() { return "Itm " + (value instanceof String ? (String) value : value instanceof String[] ? Arrays.toString((String[]) value) : ""); } } }