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