001/* 002 * flattr4j - A Java library for Flattr 003 * 004 * Copyright (C) 2011 Richard "Shred" Körber 005 * http://flattr4j.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU General Public License / GNU Lesser 009 * General Public License as published by the Free Software Foundation, 010 * either version 3 of the License, or (at your option) any later version. 011 * 012 * Licensed under the Apache License, Version 2.0 (the "License"); 013 * you may not use this file except in compliance with the License. 014 * 015 * This program is distributed in the hope that it will be useful, 016 * but WITHOUT ANY WARRANTY; without even the implied warranty of 017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 018 */ 019package org.shredzone.flattr4j.web.builder; 020 021import java.io.Serializable; 022import java.net.URL; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Map; 026import java.util.TreeMap; 027 028import org.shredzone.flattr4j.model.Category; 029import org.shredzone.flattr4j.model.CategoryId; 030import org.shredzone.flattr4j.model.Language; 031import org.shredzone.flattr4j.model.LanguageId; 032import org.shredzone.flattr4j.model.Submission; 033import org.shredzone.flattr4j.model.Thing; 034import org.shredzone.flattr4j.model.UserId; 035import org.shredzone.flattr4j.web.ButtonType; 036 037/** 038 * Builds a Flattr button tag. 039 * <p> 040 * The builder uses sensible default settings that can be changed by using its methods. 041 * All methods return a reference to the builder itself, so method calls can be 042 * daisy-chained. 043 * <p> 044 * Example: <code>String button = new ButtonBuilder().url(myUrl).toString();</code> 045 * 046 * @author Richard "Shred" Körber 047 */ 048public class ButtonBuilder implements Serializable { 049 private static final long serialVersionUID = -3066039824282852155L; 050 051 private static final int MIN_TITLE_LENGTH = 5; 052 private static final int MAX_TITLE_LENGTH = 100; 053 054 private static final int MIN_DESCRIPTION_LENGTH = 5; 055 private static final int MAX_DESCRIPTION_LENGTH = 1000; 056 057 private String url; 058 private String uid; 059 private String title; 060 private String description; 061 private String category; 062 private String language; 063 private ArrayList<String> tags = new ArrayList<String>(); 064 private String revsharekey; 065 private ButtonType type; 066 private boolean hidden = false; 067 private Boolean popout = null; 068 private String style; 069 private String styleClass; 070 private boolean html5 = false; 071 private String prefix = "data-flattr"; 072 private TreeMap<String, String> attributes = new TreeMap<String, String>(); 073 074 /** 075 * Unique URL to the thing. Always required! 076 */ 077 public ButtonBuilder url(String url) { 078 this.url = url; 079 return this; 080 } 081 082 /** 083 * Unique URL to the thing. Always required! This is a convenience method that accepts 084 * {@link URL} objects. 085 */ 086 public ButtonBuilder url(URL url) { 087 return url(url.toString()); 088 } 089 090 /** 091 * User who published the thing. Required for autosubmit. Optional if the thing is 092 * already published at Flattr. 093 */ 094 public ButtonBuilder user(UserId user) { 095 this.uid = user.getUserId(); 096 return this; 097 } 098 099 /** 100 * Title of the thing. Required for autosubmit. If a title is set, it must currently 101 * be between 5 and 100 characters long, and must not contain HTML. 102 */ 103 public ButtonBuilder title(String title) { 104 if (title.length() < MIN_TITLE_LENGTH) { 105 throw new IllegalArgumentException("title must have at least " 106 + MIN_TITLE_LENGTH + " characters."); 107 } 108 if (title.length() > MAX_TITLE_LENGTH) { 109 throw new IllegalArgumentException("title must not exceed " 110 + MAX_TITLE_LENGTH + " characters."); 111 } 112 this.title = title; 113 return this; 114 } 115 116 /** 117 * Description of the thing. Required for autosubmit. If a description is set, it must 118 * currently be between 5 and 1000 characters long, and must not contain HTML (except 119 * of <br> which is converted to newline). 120 */ 121 public ButtonBuilder description(String description) { 122 if (description.length() < MIN_DESCRIPTION_LENGTH) { 123 throw new IllegalArgumentException("description must have at least " 124 + MIN_DESCRIPTION_LENGTH + " characters."); 125 } 126 if (description.length() > MAX_DESCRIPTION_LENGTH) { 127 throw new IllegalArgumentException("description must not exceed " 128 + MAX_DESCRIPTION_LENGTH + " characters."); 129 } 130 this.description = description; 131 return this; 132 } 133 134 /** 135 * Convenience method that automatically truncates the description to its maximum 136 * accepted length. 137 */ 138 public ButtonBuilder descriptionTruncate(String description) { 139 String desc = description; 140 if (desc.length() > MAX_DESCRIPTION_LENGTH) { 141 desc = desc.substring(0, desc.length()); 142 } 143 return description(desc); 144 } 145 146 /** 147 * The category the thing is categorized with. Required for autosubmit. 148 */ 149 public ButtonBuilder category(CategoryId category) { 150 this.category = category.getCategoryId(); 151 return this; 152 } 153 154 /** 155 * Specifies the language the thing is published in. Optional. 156 */ 157 public ButtonBuilder language(LanguageId language) { 158 this.language = language.getLanguageId(); 159 return this; 160 } 161 162 /** 163 * A tag that further describes the thing. Optional. Tags must not contain ',', ';' 164 * and other non-word characters. 165 */ 166 public ButtonBuilder tag(String tag) { 167 if (tag.indexOf(',') >= 0 || tag.indexOf(';') >= 0) { 168 throw new IllegalArgumentException("illegal character in tag \"" + tag + '"'); 169 } 170 tags.add(tag); 171 return this; 172 } 173 174 /** 175 * Convenience method that adds a collection of tags. 176 */ 177 public ButtonBuilder tags(Collection<String> tags) { 178 for (String tag : tags) { 179 tag(tag); 180 } 181 return this; 182 } 183 184 /** 185 * Sets the revenue share key to be used. 186 * 187 * @since 2.5 188 */ 189 public ButtonBuilder revsharekey(String key) { 190 this.revsharekey = key; 191 return this; 192 } 193 194 /** 195 * Selects the button type to be used. Optional, defaults to the default button. 196 */ 197 public ButtonBuilder button(ButtonType type) { 198 this.type = type; 199 return this; 200 } 201 202 /** 203 * Sets whether to override the popout default and show a popout when hovering with 204 * the mouse over the button. 205 * 206 * @param popout 207 * {@code true}: always show a popout, {@code false}: never show a popout 208 * @since 2.2 209 */ 210 public ButtonBuilder popout(boolean popout) { 211 this.popout = popout; 212 return this; 213 } 214 215 /** 216 * The thing shall not be listed at Flattr. Only used on autosubmit. 217 */ 218 public ButtonBuilder hidden() { 219 this.hidden = true; 220 return this; 221 } 222 223 /** 224 * Initializes the builder based on the given {@link Thing}. This is a 225 * convenience method to prepare a link to an existing Thing. 226 */ 227 public ButtonBuilder thing(Thing thing) { 228 url(thing.getUrl()); 229 title(thing.getTitle()); 230 description(thing.getDescription()); 231 category(Category.withId(thing.getCategoryId())); 232 language(Language.withId(thing.getLanguageId())); 233 tags(thing.getTags()); 234 if (thing.isHidden()) hidden(); 235 return this; 236 } 237 238 /** 239 * Initializes the builder based on the given {@link Submission}. This is a 240 * convenience method to prepare an autosubmit of a thing. Invoke 241 * {@link #user(UserId)} together with this method for a successful autosubmission. 242 */ 243 public ButtonBuilder thing(Submission thing) { 244 url(thing.getUrl()); 245 title(thing.getTitle()); 246 description(thing.getDescription()); 247 category(thing.getCategory()); 248 language(thing.getLanguage()); 249 tags(thing.getTags()); 250 if (thing.isHidden()) hidden(); 251 return this; 252 } 253 254 /** 255 * CSS style to be used. 256 */ 257 public ButtonBuilder style(String style) { 258 this.style = style; 259 return this; 260 } 261 262 /** 263 * CSS class to be used. The class "FlattrButton" is always used. 264 */ 265 public ButtonBuilder styleClass(String styleClass) { 266 this.styleClass = styleClass; 267 return this; 268 } 269 270 /** 271 * Adds a custom HTML attribute to the generated link tag. If an attribute has already 272 * been added, its value will be replaced. Attributes are written to the tag in 273 * alphabetical order. 274 * <p> 275 * Attributes are added without further checks. It is your responsibility to take care 276 * for HTML compliance. 277 * 278 * @param attribute 279 * HTML attribute to be added 280 * @param value 281 * Value of that attribute. The builder takes care for proper HTML 282 * escaping. 283 */ 284 public ButtonBuilder attribute(String attribute, String value) { 285 String check = attribute.trim().toLowerCase(); 286 if ("class".equals(check) || "style".equals(check) || "title".equals(check) 287 || "rel".equals(check) || "href".equals(check) || "lang".equals(check) 288 || check.startsWith(prefix)) { 289 throw new IllegalArgumentException("attribute \"" + check + "\" is reserved"); 290 } 291 292 this.attributes.put(attribute, value); 293 return this; 294 } 295 296 /** 297 * Generate a HTML5 compliant button. 298 */ 299 public ButtonBuilder html5() { 300 this.html5 = true; 301 return this; 302 } 303 304 /** 305 * Sets a HTML5 key prefix. By default "data-flattr" is used. The setting is only used 306 * in HTML5 mode. 307 * 308 * @param prefix 309 * HTML5 key prefix. The string must start with "data-" 310 */ 311 public ButtonBuilder prefix(String prefix) { 312 if (!prefix.startsWith("data-")) { 313 throw new IllegalArgumentException("prefix must start with \"data-\""); 314 } 315 this.prefix = prefix; 316 return this; 317 } 318 319 /** 320 * Builds a button of the current setup. 321 */ 322 @Override 323 public String toString() { 324 if (url == null) { 325 throw new IllegalStateException("url is required, but missing"); 326 } 327 328 StringBuilder sb = new StringBuilder(); 329 sb.append("<a"); 330 331 sb.append(" class=\"FlattrButton"); 332 if (styleClass != null) { 333 sb.append(' ').append(escape(styleClass)); 334 } 335 sb.append('"'); 336 337 if (style != null) { 338 sb.append(" style=\"").append(escape(style)).append('"'); 339 } 340 341 sb.append(" href=\"").append(escape(url)).append('"'); 342 343 if (title != null) { 344 sb.append(" title=\"").append(escape(title)).append('"'); 345 } 346 347 if (language != null) { 348 sb.append(" lang=\"").append(escape(language)).append('"'); 349 } 350 351 if (html5) { 352 appendHtml5(sb); 353 } else { 354 appendAttributes(sb); 355 } 356 357 if (!attributes.isEmpty()) { 358 for (Map.Entry<String, String> entry : attributes.entrySet()) { 359 sb.append(' ').append(entry.getKey()).append("=\""); 360 sb.append(escape(entry.getValue())); 361 sb.append('"'); 362 } 363 } 364 365 sb.append('>'); 366 367 if (description != null) { 368 sb.append(description); 369 } 370 371 sb.append("</a>"); 372 373 return sb.toString(); 374 } 375 376 /** 377 * Appends thing attributes to the {@link StringBuilder}. 378 * 379 * @param sb 380 * {@link StringBuilder} to append the attributes to 381 */ 382 private void appendAttributes(StringBuilder sb) { 383 boolean header = false; 384 385 if (uid != null) { 386 appendAttributesHeader(sb); 387 header = true; 388 sb.append("uid:").append(escape(uid)).append(';'); 389 } 390 391 if (category != null) { 392 if (!header) appendAttributesHeader(sb); 393 header = true; 394 sb.append("category:").append(escape(category)).append(';'); 395 } 396 397 if (!tags.isEmpty()) { 398 if (!header) appendAttributesHeader(sb); 399 header = true; 400 sb.append("tags:"); 401 appendTagList(sb); 402 sb.append(';'); 403 } 404 405 if (revsharekey != null) { 406 if (!header) appendAttributesHeader(sb); 407 header = true; 408 sb.append("revsharekey:").append(escape(revsharekey)).append(';'); 409 } 410 411 if (type != null && type == ButtonType.COMPACT) { 412 if (!header) appendAttributesHeader(sb); 413 header = true; 414 sb.append("button:compact;"); 415 } 416 417 if (popout != null) { 418 if (!header) appendAttributesHeader(sb); 419 header = true; 420 sb.append("popout:").append(popout.booleanValue() ? 1 : 0).append(';'); 421 } 422 423 if (hidden) { 424 if (!header) appendAttributesHeader(sb); 425 header = true; 426 sb.append("hidden:1;"); 427 } 428 429 if (header) { 430 sb.append('"'); 431 } 432 } 433 434 /** 435 * Appends the header for non-http5 attributes. 436 * 437 * @param sb 438 * {@link StringBuilder} to append the attributes to 439 */ 440 private void appendAttributesHeader(StringBuilder sb) { 441 sb.append(" rel=\"flattr;"); 442 } 443 444 /** 445 * Appends thing attributes as HTML 5 attributes {@link StringBuilder}. 446 * 447 * @param sb 448 * {@link StringBuilder} to append the attributes to 449 */ 450 private void appendHtml5(StringBuilder sb) { 451 if (uid != null) { 452 appendHtml5Attribute(sb, "uid", uid); 453 } 454 455 if (category != null) { 456 appendHtml5Attribute(sb, "category", category); 457 } 458 459 if (!tags.isEmpty()) { 460 sb.append(' ').append(prefix).append("-tags=\""); 461 appendTagList(sb); 462 sb.append('"'); 463 } 464 465 if (revsharekey != null) { 466 appendHtml5Attribute(sb, "revsharekey", revsharekey); 467 } 468 469 if (type != null && type == ButtonType.COMPACT) { 470 appendHtml5Attribute(sb, "button", "compact"); 471 } 472 473 if (popout != null) { 474 appendHtml5Attribute(sb, "popout", popout.booleanValue() ? "1" : "0"); 475 } 476 477 if (hidden) { 478 appendHtml5Attribute(sb, "hidden", "1"); 479 } 480 } 481 482 /** 483 * Appends a single HTML5 attribute. 484 * 485 * @param sb 486 * {@link StringBuilder} to append the attribute to 487 * @param key 488 * Attribute key 489 * @param value 490 * Value (unescaped) 491 */ 492 private void appendHtml5Attribute(StringBuilder sb, String key, String value) { 493 sb.append(' ').append(prefix).append('-').append(key).append("=\""); 494 sb.append(escape(value)).append('"'); 495 } 496 497 /** 498 * Appends a list of all tags, separated by comma. 499 * 500 * @param sb 501 * {@link StringBuilder} to append the tags to 502 */ 503 private void appendTagList(StringBuilder sb) { 504 boolean needsSeparator = false; 505 for (String tag : tags) { 506 if (needsSeparator) sb.append(','); 507 sb.append(escape(tag)); 508 needsSeparator = true; 509 } 510 } 511 512 /** 513 * Escapes a string for use in HTML attributes. 514 * 515 * @param str 516 * Attribute to be escaped 517 * @return Escaped attribute 518 */ 519 private String escape(String str) { 520 return str.replace("&", "&").replace("<", "<").replace("\"", """); 521 } 522 523}