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}