VCard format: correctly co/decode escaped values
[jvcard.git] / src / be / nikiroo / jvcard / parsers / Vcard21Parser.java
CommitLineData
a3b510ab
NR
1package be.nikiroo.jvcard.parsers;
2
bcb54330 3import java.util.Iterator;
a3b510ab
NR
4import java.util.LinkedList;
5import java.util.List;
6
7import be.nikiroo.jvcard.Card;
8import be.nikiroo.jvcard.Contact;
9import be.nikiroo.jvcard.Data;
10import be.nikiroo.jvcard.TypeInfo;
11
12public class Vcard21Parser {
0b6140e4
NR
13 /**
14 * Load the given data from under the given {@link Format}.
15 *
16 * @param lines
17 * the input to load from
18 * @param format
19 * the {@link Format} to load as
20 *
21 * @return the list of elements
22 */
23 public static List<Contact> parseContact(Iterable<String> textData) {
bcb54330 24 Iterator<String> lines = textData.iterator();
a3b510ab 25 List<Contact> contacts = new LinkedList<Contact>();
0b6140e4 26 List<String> datas = null;
a3b510ab 27
bcb54330
NR
28 String nextRawLine = null;
29 if (lines.hasNext()) {
30 nextRawLine = lines.next();
31 while (lines.hasNext() && isContinuation(nextRawLine)) {
32 // BAD INPUT FILE. IGNORE.
33 System.err
34 .println("VCARD Parser warning: CONTINUATION line seen before any data line");
35 nextRawLine = lines.next();
36 }
37 }
38
39 while (nextRawLine != null) {
40 StringBuilder rawLine = new StringBuilder(nextRawLine.trim());
41 if (lines.hasNext())
42 nextRawLine = lines.next();
43 else
44 nextRawLine = null;
45
46 while (isContinuation(nextRawLine)) {
47 rawLine.append(nextRawLine.trim());
48 if (lines.hasNext())
49 nextRawLine = lines.next();
50 else
51 nextRawLine = null;
52 }
53
54 String line = rawLine.toString();
a3b510ab 55 if (line.equals("BEGIN:VCARD")) {
0b6140e4 56 datas = new LinkedList<String>();
a3b510ab
NR
57 } else if (line.equals("END:VCARD")) {
58 if (datas == null) {
59 // BAD INPUT FILE. IGNORE.
60 System.err
61 .println("VCARD Parser warning: END:VCARD seen before any VCARD:BEGIN");
62 } else {
0b6140e4 63 contacts.add(new Contact(parseData(datas)));
a3b510ab
NR
64 }
65 } else {
66 if (datas == null) {
67 // BAD INPUT FILE. IGNORE.
68 System.err
69 .println("VCARD Parser warning: data seen before any VCARD:BEGIN");
70 } else {
0b6140e4
NR
71 datas.add(line);
72 }
73 }
74 }
75
76 return contacts;
77 }
78
79 /**
80 * Load the given data from under the given {@link Format}.
81 *
82 * @param lines
83 * the input to load from
84 * @param format
85 * the {@link Format} to load as
86 *
87 * @return the list of elements
88 */
89 public static List<Data> parseData(Iterable<String> textData) {
90 List<Data> datas = new LinkedList<Data>();
91
92 for (String line : textData) {
93 List<TypeInfo> types = new LinkedList<TypeInfo>();
94 String name = "";
95 String value = "";
96 String group = "";
97
98 if (line.contains(":")) {
99 int colIndex = line.indexOf(':');
100 String rest = line.substring(0, colIndex);
101 value = line.substring(colIndex + 1);
102
103 if (rest.contains(";")) {
104 String tab[] = rest.split(";");
105 name = tab[0];
106
107 for (int i = 1; i < tab.length; i++) {
108 if (tab[i].contains("=")) {
109 int equIndex = tab[i].indexOf('=');
110 String tname = tab[i].substring(0, equIndex);
111 String tvalue = tab[i].substring(equIndex + 1);
112 types.add(new TypeInfo(tname, tvalue));
a3b510ab 113 } else {
0b6140e4 114 types.add(new TypeInfo(tab[i], ""));
a3b510ab 115 }
a3b510ab 116 }
0b6140e4
NR
117 } else {
118 name = rest;
a3b510ab 119 }
0b6140e4
NR
120 } else {
121 name = line;
122 }
123
124 if (name.contains(".")) {
125 int dotIndex = name.indexOf('.');
126 group = name.substring(0, dotIndex);
127 name = name.substring(dotIndex + 1);
a3b510ab 128 }
0b6140e4
NR
129
130 datas.add(new Data(types, name, value, group));
a3b510ab
NR
131 }
132
0b6140e4 133 return datas;
a3b510ab
NR
134 }
135
cf77cb35
NR
136 /**
137 * Return a {@link String} representation of the given {@link Card}, line by
138 * line.
139 *
140 * @param card
141 * the card to convert
142 *
0b6140e4
NR
143 * @return the {@link String} representation
144 */
145 public static List<String> toStrings(Card card) {
146 List<String> lines = new LinkedList<String>();
147
148 for (Contact contact : card) {
149 lines.addAll(toStrings(contact, -1));
150 }
151
152 return lines;
153 }
154
155 /**
156 * Return a {@link String} representation of the given {@link Contact}, line
157 * by line.
158 *
159 * @param card
160 * the contact to convert
161 *
cf77cb35
NR
162 * @param startingBKey
163 * the starting BKey number (all the other will follow) or -1 for
164 * no BKey
165 *
166 * @return the {@link String} representation
167 */
168 public static List<String> toStrings(Contact contact, int startingBKey) {
169 List<String> lines = new LinkedList<String>();
170
171 lines.add("BEGIN:VCARD");
172 lines.add("VERSION:2.1");
1c03abaf 173 for (Data data : contact) {
0b6140e4 174 lines.addAll(toStrings(data));
a3b510ab 175 }
cf77cb35 176 lines.add("END:VCARD");
a3b510ab 177
cf77cb35 178 return lines;
a3b510ab
NR
179 }
180
cf77cb35 181 /**
0b6140e4 182 * Return a {@link String} representation of the given {@link Data}, line by
cf77cb35
NR
183 * line.
184 *
0b6140e4
NR
185 * @param data
186 * the data to convert
cf77cb35
NR
187 *
188 * @return the {@link String} representation
189 */
0b6140e4 190 public static List<String> toStrings(Data data) {
cf77cb35 191 List<String> lines = new LinkedList<String>();
a3b510ab 192
0b6140e4
NR
193 StringBuilder dataBuilder = new StringBuilder();
194 if (data.getGroup() != null && !data.getGroup().trim().equals("")) {
195 dataBuilder.append(data.getGroup().trim());
196 dataBuilder.append('.');
197 }
198 dataBuilder.append(data.getName());
199 for (TypeInfo type : data) {
200 dataBuilder.append(';');
201 dataBuilder.append(type.getName());
202 if (type.getValue() != null && !type.getValue().trim().equals("")) {
203 dataBuilder.append('=');
aecb3399 204 dataBuilder.append(type.getRawValue());
0b6140e4
NR
205 }
206 }
207 dataBuilder.append(':');
208
209 // TODO: bkey!
aecb3399 210 dataBuilder.append(data.getRawValue());
0b6140e4
NR
211
212 // RFC says: Content lines SHOULD be folded to a maximum width of 75
213 // octets -> since it is SHOULD, we will just cut it as 74/75 chars
214 // depending if the last one fits in one char (note: chars != octet)
215 boolean continuation = false;
216 while (dataBuilder.length() > 0) {
217 int stop = 74;
218 if (continuation)
219 stop--; // the space takes 1
220 if (dataBuilder.length() > stop) {
221 char car = dataBuilder.charAt(stop - 1);
222 // RFC forbids cutting a character in 2
223 if (Character.isHighSurrogate(car)) {
224 stop++;
225 }
226 }
227
228 stop = Math.min(stop, dataBuilder.length());
229 if (continuation) {
230 lines.add(' ' + dataBuilder.substring(0, stop));
231 } else {
232 lines.add(dataBuilder.substring(0, stop));
233 }
234 dataBuilder.delete(0, stop);
235
236 continuation = true;
a3b510ab 237 }
78e4af97 238
cf77cb35 239 return lines;
a3b510ab 240 }
bcb54330 241
5ad0e17e
NR
242 /**
243 * Clone the given {@link Card} by exporting then importing it again in VCF.
244 *
245 * @param c
246 * the {@link Card} to clone
247 *
248 * @return the clone {@link Contact}
249 */
250 public static Card clone(Card c) {
251 return new Card(parseContact(toStrings(c)));
252 }
253
254 /**
255 * Clone the given {@link Contact} by exporting then importing it again in
256 * VCF.
257 *
258 * @param c
259 * the {@link Contact} to clone
260 *
261 * @return the clone {@link Contact}
262 */
263 public static Contact clone(Contact c) {
264 return parseContact(toStrings(c, -1)).get(0);
265 }
266
bcb54330
NR
267 /**
268 * Check if the given line is a continuation line or not.
269 *
270 * @param line
271 * the line to check
272 *
273 * @return TRUE if the line is a continuation line
274 */
275 private static boolean isContinuation(String line) {
276 if (line != null && line.length() > 0)
277 return (line.charAt(0) == ' ' || line.charAt(0) == '\t');
278 return false;
279 }
a3b510ab 280}