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) : ""); }
}
}