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
40
41
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
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
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
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(" "));
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
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
383
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
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
411
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