001package ezvcard.io.html;
002
003import java.io.IOException;
004import java.io.OutputStream;
005import java.io.OutputStreamWriter;
006import java.io.StringWriter;
007import java.io.UncheckedIOException;
008import java.io.Writer;
009import java.nio.file.Files;
010import java.nio.file.Path;
011import java.time.Instant;
012import java.time.ZoneId;
013import java.time.ZoneOffset;
014import java.time.format.DateTimeFormatter;
015import java.time.temporal.Temporal;
016import java.util.ArrayList;
017import java.util.HashMap;
018import java.util.List;
019import java.util.Map;
020import java.util.regex.Pattern;
021
022import ezvcard.Ezvcard;
023import ezvcard.VCard;
024import ezvcard.io.scribe.ScribeIndex;
025import ezvcard.parameter.ImageType;
026import ezvcard.property.Photo;
027import ezvcard.util.DataUri;
028import ezvcard.util.VCardDateFormat;
029import ezvcard.util.VCardFloatFormatter;
030import freemarker.template.Configuration;
031import freemarker.template.Template;
032import freemarker.template.TemplateException;
033
034/*
035 Copyright (c) 2012-2026, Michael Angstadt
036 All rights reserved.
037
038 Redistribution and use in source and binary forms, with or without
039 modification, are permitted provided that the following conditions are met: 
040
041 1. Redistributions of source code must retain the above copyright notice, this
042 list of conditions and the following disclaimer. 
043 2. Redistributions in binary form must reproduce the above copyright notice,
044 this list of conditions and the following disclaimer in the documentation
045 and/or other materials provided with the distribution. 
046
047 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
048 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
049 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
050 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
051 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
052 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
053 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
054 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
055 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
056 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
057
058 The views and conclusions contained in the software and documentation are those
059 of the authors and should not be interpreted as representing official policies, 
060 either expressed or implied, of the FreeBSD Project.
061 */
062
063/**
064 * <p>
065 * Writes {@link VCard} objects to a templated HTML page (hCard format).
066 * </p>
067 * <p>
068 * <b>Example:</b>
069 * </p>
070 * 
071 * <pre class="brush:java">
072 * VCard vcard1 = ...
073 * VCard vcard2 = ...
074 * 
075 * HCardPage page = new HCardPage();
076 * page.add(vcard1);
077 * page.add(vcard2);
078 * 
079 * File file = new File("hcard.html");
080 * page.write(file);
081 * </pre>
082 * @author Michael Angstadt
083 * @see <a
084 * href="http://microformats.org/wiki/hcard">http://microformats.org/wiki/hcard</a>
085 */
086public class HCardPage {
087        private final Template template;
088        private final List<VCard> vcards = new ArrayList<>();
089
090        /**
091         * Creates a new hCard page that uses the default template.
092         */
093        public HCardPage() {
094                Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
095                cfg.setClassForTemplateLoading(HCardPage.class, "");
096                cfg.setWhitespaceStripping(true);
097                try {
098                        template = cfg.getTemplate("hcard-template.html");
099                } catch (IOException e) {
100                        //should never be thrown because it's always on the classpath
101                        throw new UncheckedIOException(e);
102                }
103        }
104
105        /**
106         * Creates a new hCard page using a custom template. See the <a href=
107         * "https://github.com/mangstadt/ez-vcard/blob/master/src/main/resources/ezvcard/io/html/hcard-template.html"
108         * >default template</a> for an example.
109         * @param template the template to use
110         */
111        public HCardPage(Template template) {
112                this.template = template;
113        }
114
115        /**
116         * Adds a vCard to the HTML page.
117         * @param vcard the vCard to add
118         */
119        public void add(VCard vcard) {
120                vcards.add(vcard);
121        }
122
123        /**
124         * Writes the HTML document to a string.
125         * @return the HTML document
126         * @throws TemplateException if there's a problem processing the template
127         */
128        public String write() throws TemplateException {
129                StringWriter sw = new StringWriter();
130                try {
131                        write(sw);
132                } catch (IOException e) {
133                        //should never thrown because we're writing to a string
134                        throw new UncheckedIOException(e);
135                }
136                return sw.toString();
137        }
138
139        /**
140         * Writes the HTML document to an output stream.
141         * @param out the output stream
142         * @throws IOException if there's a problem writing to the output stream
143         * @throws TemplateException if there's a problem processing the template
144         */
145        public void write(OutputStream out) throws IOException, TemplateException {
146                write(new OutputStreamWriter(out));
147        }
148
149        /**
150         * Writes the HTML document to a file.
151         * @param file the file
152         * @throws IOException if there's a problem writing to the file
153         * @throws TemplateException if there's a problem processing the template
154         */
155        public void write(Path file) throws IOException, TemplateException {
156                try (Writer writer = Files.newBufferedWriter(file)) {
157                        write(writer);
158                }
159        }
160
161        /**
162         * Writes the HTML document to a writer.
163         * @param writer the writer
164         * @throws IOException if there's a problem writing to the writer
165         * @throws TemplateException if there's a problem processing the template
166         */
167        public void write(Writer writer) throws IOException, TemplateException {
168                Map<String, Object> map = new HashMap<>();
169                map.put("vcards", vcards);
170                map.put("utils", new TemplateUtils());
171                map.put("translucentBg", readImage("translucent-bg.png", ImageType.PNG));
172                map.put("noProfile", readImage("no-profile.png", ImageType.PNG));
173                map.put("ezVCardVersion", Ezvcard.VERSION);
174                map.put("ezVCardUrl", Ezvcard.URL);
175                map.put("scribeIndex", new ScribeIndex());
176
177                template.process(map, writer);
178                writer.flush();
179        }
180
181        /**
182         * Reads an image from the classpath.
183         * @param name the file name, relative to this class
184         * @param mediaType the media type of the image
185         * @return the image
186         * @throws IOException if there's a problem reading the image
187         */
188        private Photo readImage(String name, ImageType mediaType) throws IOException {
189                return new Photo(getClass().getResourceAsStream(name), mediaType);
190        }
191
192        /**
193         * Utility functions for the freemarker template.
194         */
195        public static class TemplateUtils {
196                private final Pattern newlineRegex = Pattern.compile("\\r\\n|\\r|\\n");
197                private final VCardFloatFormatter floatFormatter = new VCardFloatFormatter(6);
198
199                public String base64(String contentType, byte[] data) {
200                        return new DataUri(contentType, data).toString();
201                }
202
203                public String lineBreaks(String value) {
204                        return newlineRegex.matcher(value).replaceAll("<br />");
205                }
206
207                public String format(Temporal temporal, String format) {
208                        return format(temporal, format, ZoneOffset.UTC);
209                }
210
211                public String formatLocal(Temporal temporal, String format) {
212                        return format(temporal, format, ZoneId.systemDefault());
213                }
214
215                private String format(Temporal temporal, String format, ZoneId offset) {
216                        if (temporal instanceof Instant) {
217                                temporal = ((Instant) temporal).atZone(offset);
218                        }
219                        return DateTimeFormatter.ofPattern(format).format(temporal);
220                }
221
222                public String format(ZoneOffset offset) {
223                        return VCardDateFormat.BASIC.format(offset);
224                }
225
226                public String format(Double d) {
227                        return floatFormatter.format(d);
228                }
229        }
230}