View Javadoc
1   package pl.matsuo.core.web.view;
2   
3   import com.google.common.base.Joiner;
4   import org.springframework.stereotype.Component;
5   import pl.matsuo.core.model.validation.EntityReference;
6   import pl.matsuo.core.model.validation.PasswordField;
7   
8   import javax.annotation.PostConstruct;
9   import javax.persistence.ManyToOne;
10  import javax.persistence.OneToOne;
11  import javax.validation.Validation;
12  import javax.validation.Validator;
13  import javax.validation.constraints.Digits;
14  import javax.validation.constraints.NotNull;
15  import javax.validation.constraints.Pattern;
16  import javax.validation.metadata.BeanDescriptor;
17  import javax.validation.metadata.ConstraintDescriptor;
18  import java.lang.annotation.Annotation;
19  import java.lang.reflect.AnnotatedElement;
20  import java.lang.reflect.Method;
21  import java.sql.Time;
22  import java.util.ArrayList;
23  import java.util.Date;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.logging.Logger;
29  
30  import static java.beans.Introspector.*;
31  import static java.util.Arrays.*;
32  import static org.apache.commons.lang3.ArrayUtils.*;
33  import static org.springframework.util.StringUtils.*;
34  import static pl.matsuo.core.util.ReflectUtil.*;
35  import static pl.matsuo.core.util.collection.ArrayUtil.*;
36  
37  
38  /**
39   * Zbiór metod automatyzujących generowanie kodu jsp.
40   * @author Marek Romanowski
41   * @since 09-06-2013
42   */
43  @Component
44  public class BootstrapRenderer {
45    private static Logger logger = Logger.getLogger(BootstrapRenderer.class.getName());
46  
47  
48    private static BootstrapRenderer bootstrapRenderer;
49  
50  
51    Validator validator;
52  
53  
54    @PostConstruct
55    public void init() {
56      if (bootstrapRenderer != null) {
57        logger.severe("Second spring bean BootstrapRenderer");
58      }
59  
60      bootstrapRenderer = this;
61  
62      validator = Validation.buildDefaultValidatorFactory().getValidator();
63    }
64  
65  
66    protected String renderField(Class<?> fieldType, AnnotatedElement annotatedElement,
67        String fieldName, BootstrapRenderingBuilder builder) {
68      String fullFieldName = fullFieldName(builder.entityName, fieldName);
69  
70      return createControlGroup(fullFieldName, createControls(
71          fieldType, annotatedElement, fieldName, builder.entityType, fullFieldName, builder,
72          // css classes
73          addAll(builder.cssClasses, lastNameElement(fieldName.split("[.]")))));
74    }
75  
76  
77    private String createControlGroup(String fullFieldName, HtmlElement controls) {
78      return div(asList("form-group", fullFieldName.replaceAll("[.-]", "_"),
79              bindServerErrorPath(fullFieldName, " && 'error' || ''")),
80          el("label", asList("col-sm-4 control-label"))
81                .attr("for", fullFieldName)
82                .attr("translate", fullFieldName),
83          controls
84          ).toString();
85    }
86  
87  
88    private String serverErrorPath(String fullFieldName) {
89      return formField(fullFieldName, "serverError");
90    }
91  
92  
93    private String bindServerErrorPath(String fullFieldName, String suffix) {
94      return "{{" + formField(fullFieldName, "serverError") + suffix + "}}";
95    }
96  
97  
98    private String formField(String fullFieldName, String field) {
99      return joinDot("form", fullFieldName.replaceAll("[.-]", "_"), field);
100   }
101 
102 
103   private String joinDot(String ... parts) {
104     return Joiner.on(".").join(parts);
105   }
106 
107 
108   /**
109    * Tworzy pole formularza razem z helpem, opisem itp.
110    */
111   private HtmlElement createControls(Class<?> fieldType, AnnotatedElement annotatedElement, String fieldName,
112                                      Class<?> entityType, String fullFieldName, BootstrapRenderingBuilder builder,
113                                      String... cssClasses) {
114     return div(asList("col-sm-6", isCheckbox(fieldType) ? "checkbox" : ""),
115         createInput(fieldType, annotatedElement, fullFieldName, entityType, fieldName, builder, cssClasses),
116         el("span", asList("help-inline", bindServerErrorPath(fullFieldName, " ? '' : 'hide'")),
117             text(bindServerErrorPath(fullFieldName, ""))));
118   }
119 
120 
121   private String lastNameElement(String[] splitted) {
122     String lastNameElement = splitted[splitted.length - 1];
123     if (lastNameElement.equals("id")) {
124       lastNameElement = lastNameElement + capitalize(splitted[splitted.length - 2]);
125     }
126 
127     return lastNameElement;
128   }
129 
130 
131   private boolean isCheckbox(Class<?> fieldType) {
132     return boolean.class.isAssignableFrom(fieldType) || Boolean.class.isAssignableFrom(fieldType);
133   }
134 
135 
136   protected HtmlElement createSelect(String lastNameElement, String constantValues) {
137     HtmlElement element = el("ui-select", asList(""),
138           el("ui-select-match", asList(""),
139               text("{{ formatElement($select.selected) }}"))
140               .attr("placeholder", "{{ opts.placeholderText | translate }}"),
141           el("ui-select-choices", asList(""),
142             div(asList(""))
143               .attr("ng-bind-html", "formatElement(item)"))
144             .attr("repeat", "item in " + constantValues + " | filter: $select.search")
145             .attr("refresh", "searchElements($select.search)"))
146         .attr("mt-select-options", joinDot(lastNameElement, "options"))
147         .attr("ng-disabled", joinDot(lastNameElement, "options.disabled"));
148     return element;
149   }
150 
151 
152   /**
153    * Tworzy kontrolkę formularza.
154    */
155   private HtmlPart createInput(Class<?> fieldType, AnnotatedElement annotatedElement, String fullFieldName,
156                                Class<?> entityType, String fieldName, BootstrapRenderingBuilder builder,
157                                String... cssClasses) {
158     boolean addFormControlStyle = true;
159     HtmlElement el;
160     HtmlElement inputIfNotEl = null;
161     String ngModel = fullFieldName;
162 
163     if (Enum.class.isAssignableFrom(fieldType)) {
164       el = el("select", asList(""),
165           getEnumValuesElements((Class<? extends Enum<?>>) fieldType,
166               !isAnnotationPresent(annotatedElement, NotNull.class)));
167     } else if (Time.class.isAssignableFrom(fieldType)) {
168       el = el("input", asList("input-size-time", "timepicker"))
169           .attr("type", "text")
170           .attr("placeholder", "HH:mm");
171       pattern(el, "[0-2][0-9]:[0-5][0-9]");
172     } else if (Date.class.isAssignableFrom(fieldType)) {
173       el = el("input", asList("input-size-date"))
174           .attr("type", "text")
175           .attr("mt-datepicker", "datepickerOptions");
176     } else if (isAnnotationPresent(annotatedElement, ManyToOne.class, EntityReference.class, OneToOne.class)) {
177       String[] splitted = fullFieldName.split("[.]");
178       String lastNameElement = lastNameElement(splitted);
179 
180       el = createSelect(lastNameElement, "$select.elements");
181 
182       if (fieldType.equals(Integer.class)) {
183         ngModel = joinDot(lastNameElement, "value");
184       }
185       addFormControlStyle = false;
186     } else if (isCheckbox(fieldType)) {
187       inputIfNotEl = el("input", asList("")).attr("type", "checkbox");
188       el = el("label", asList(""),
189                 inputIfNotEl,
190                 text("&nbsp;"));
191 
192       addFormControlStyle = false;
193     } else {
194       el = el("input", asList(""))
195           .attr("type", "text");
196 
197       if (Number.class.isAssignableFrom(fieldType)) {
198         pattern(el, "[0-9]+([.,][0-9]+)?");
199       }
200 
201       if (isAnnotationPresent(annotatedElement, PasswordField.class)) {
202         el.attr("type", "password");
203       }
204     }
205 
206     addFieldValidation(fieldType, entityType, el, fieldName);
207     HtmlElement val = inputIfNotEl != null ? inputIfNotEl : el;
208 
209     val.attr("id", fullFieldName)
210         .attr("name", fullFieldName.replaceAll("\\.", "_"))
211         .attr("ng-model", ngModel)
212         .attr("placeholder", "{{ '" + fullFieldName + "' | translate }}")
213         .style(cssClasses);
214 
215     if (addFormControlStyle) {
216       val.style("form-control");
217     }
218 
219     if (builder != null) {
220       for (String attr : builder.attributes.keySet()) {
221         val.attr(attr, builder.attributes.get(attr));
222       }
223     }
224 
225     return el;
226   }
227 
228 
229   private boolean isAnnotationPresent(
230       AnnotatedElement annotatedElement, Class<? extends Annotation> ... annotations) {
231     if (annotatedElement != null) {
232       for (Class<? extends Annotation> annotation : annotations) {
233         if (annotatedElement.isAnnotationPresent(annotation)) {
234           return true;
235         }
236       }
237     }
238 
239     return false;
240   }
241 
242 
243   private void addFieldValidation(Class<?> fieldType, Class<?> entityType, HtmlElement el, String fieldName) {
244     if (entityType == null) {
245       return;
246     }
247 
248     String propertyName = fieldName.substring(fieldName.lastIndexOf(".") + 1);
249     if (fieldName.contains(".")) {
250       entityType = getPropertyType(entityType, fieldName.substring(0, fieldName.lastIndexOf(".")));
251     }
252 
253     BeanDescriptor constraintsForClass = validator.getConstraintsForClass(entityType);
254     javax.validation.metadata.PropertyDescriptor constraintsForProperty =
255         constraintsForClass.getConstraintsForProperty(propertyName);
256 
257     if (constraintsForProperty != null) {
258       Set<ConstraintDescriptor<?>> constraintDescriptors = constraintsForProperty.getConstraintDescriptors();
259 
260       for (ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {
261         Object annotation = constraintDescriptor.getAnnotation();
262 
263         if (NotNull.class.isAssignableFrom(annotation.getClass())) {
264           el.attr("required", null);
265         } else if (Pattern.class.isAssignableFrom(annotation.getClass())) {
266           Pattern pattern = (Pattern) annotation;
267           pattern(el, pattern.regexp());
268         } else if (Digits.class.isAssignableFrom(annotation.getClass())) {
269           Digits digits = (Digits) annotation;
270           pattern(el, "[0-9]{" + digits.integer() + "}([.,][0-9]{" + digits.fraction() + "})?");
271         } else if (isAnnotatedAnnotation(annotation.getClass(), Pattern.class) != null) {
272           Pattern pattern = isAnnotatedAnnotation(annotation.getClass(), Pattern.class);
273           pattern(el, pattern.regexp());
274         }
275       }
276     }
277   }
278 
279 
280   private <A extends Annotation> A isAnnotatedAnnotation(Class<?> clazz, Class<A> annotation) {
281     if (clazz.getAnnotation(annotation) != null) {
282       return annotation.getClass().getAnnotation(annotation);
283     } else {
284       for (Class<?> iface : clazz.getInterfaces()) {
285         if (iface.getAnnotation(annotation) != null) {
286           return iface.getAnnotation(annotation);
287         }
288       }
289     }
290 
291     return null;
292   }
293 
294 
295   private void pattern(HtmlElement el, String pattern) {
296     el.attr("ng-pattern", "/^(" + pattern + ")?$/");
297   }
298 
299 
300   private HtmlPart[] getEnumValuesElements(Class<? extends Enum<?>> propertyType, boolean withEmptyElement) {
301     List<HtmlPart> elements = new ArrayList<>();
302 
303     if (withEmptyElement) {
304       elements.add(el("option", asList("")));
305     }
306 
307     for (Enum<?> enumElement : propertyType.getEnumConstants()) {
308       elements.add(el("option", asList(""))
309           .attr("translate", joinDot("enum", propertyType.getSimpleName(), enumElement.toString()))
310           .attr("value", enumElement.name()));
311     }
312 
313     return elements.toArray(new HtmlPart[0]);
314   }
315 
316 
317   public BootstrapRenderingBuilder create(Class<?> entityType) {
318     return new BootstrapRenderingBuilder(entityType);
319   }
320 
321 
322   /**
323    * Klasa buildera pozwalająca na zdefiniowanie parametrów renderowania pól.
324    */
325   public static class BootstrapRenderingBuilder {
326     private Class<?> entityType;
327     private String entityName = "entity";
328     private boolean inline = false;
329     private String[] cssClasses;
330     private Map<String, String> attributes = new HashMap<>();
331 
332 
333     public BootstrapRenderingBuilder(Class<?> entityType) {
334       this.entityType = entityType;
335     }
336 
337 
338     public BootstrapRenderingBuilder entityName(String entityName) {
339       this.entityName = entityName;
340       return this;
341     }
342 
343 
344     public BootstrapRenderingBuilder inline(boolean inline) {
345       this.inline = inline;
346       return this;
347     }
348 
349 
350     public BootstrapRenderingBuilder cssClasses(String ... cssClasses) {
351       this.cssClasses = cssClasses;
352       return this;
353     }
354 
355 
356     public BootstrapRenderingBuilder attribute(String name, String value) {
357       attributes.put(name, value);
358       return this;
359     }
360 
361 
362     public String render(String ... fields) {
363       if (inline) {
364         return renderer().renderInlineFields(fields, this);
365       } else {
366         return asList(fields).stream().reduce("", (sum, fieldName) -> {
367           return sum + renderer().renderField(getPropertyType(entityType, fieldName),
368               getAnnoatedElement(entityType, fieldName), fieldName, this) + "\n";
369         });
370       }
371     }
372 
373 
374     public String renderWithName(String entityFieldName, String htmlFieldName) {
375       return renderer().renderField(getPropertyType(entityType, entityFieldName),
376           getAnnoatedElement(entityType, entityFieldName), htmlFieldName, this);
377     }
378   }
379 
380 
381   /**
382    * Renderuje listę pól należących do encji <code>entityType</code>. Prefiksem nazw będzie
383    * "entity" - jako generyczne odwowłanie do  w/w obiektu.
384    */
385   private String renderInlineFields(String[] fields, BootstrapRenderingBuilder builder) {
386     List<HtmlPart> elements = new ArrayList<>();
387 
388     for (String fieldName : fields) {
389       String fullFieldName = fullFieldName(builder.entityName, fieldName);
390       String simpleElementName = last(fieldName.split("[.]"));
391 
392       elements.add(el("span", asList("inline-form-text", simpleElementName)).attr("translate", fullFieldName));
393 
394       elements.add(createInput(getPropertyType(builder.entityType, fieldName),
395                                 getAnnoatedElement(builder.entityType, fieldName),
396                                 fullFieldName, builder.entityType, "entity", builder,
397                                 // css classes
398                                 simpleElementName));
399     }
400     elements.remove(0);
401 
402     elements.add(el("span", asList("help-inline"), text(bindServerErrorPath(builder.entityName, ""))));
403 
404     return createControlGroup(fullFieldName(builder.entityName, fields[0]),
405                                  div(asList("controls"), elements.toArray(new HtmlPart[0])));
406   }
407 
408 
409   /**
410    * Renderuje pojedyncze pole typu <code>fieldType</code>, o nazwie <code>fieldName</code>, któremu
411    * opcjonalnie można przypisać dodatkowe klasy stylu <code>cssClasses</code>.
412    */
413   public String renderSingleField(Class<?> fieldType, String fieldName, String ... cssClasses) {
414     return renderField(fieldType, null, fieldName, create(null).cssClasses(cssClasses)) + "\n";
415   }
416 
417 
418   public String renderSingleField(Method method, String fieldName, String ... cssClasses) {
419     return renderField(method.getReturnType(), method, fieldName, create(null).cssClasses(cssClasses)) + "\n";
420   }
421 
422 
423   private String fullFieldName(String entityName, String fieldName) {
424     return (entityName != null ? decapitalize(entityName) + "." : "") + fieldName;
425   }
426 
427 
428   public HtmlElement div(List<String> classes, HtmlPart ... innerElements) {
429     return new HtmlElement("div", innerElements).style(classes);
430   }
431 
432 
433   public HtmlElement el(String element, List<String> classes, HtmlPart ... innerElements) {
434     return new HtmlElement(element, innerElements).style(classes);
435   }
436 
437 
438   public HtmlText text(String text) {
439     return new HtmlText(text);
440   }
441 
442 
443   public static BootstrapRenderer renderer() {
444     return bootstrapRenderer;
445   }
446 }
447