You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ComponentSizeValidator.java 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.server;
  17. import java.io.PrintStream;
  18. import java.io.Serializable;
  19. import java.util.ArrayDeque;
  20. import java.util.ArrayList;
  21. import java.util.Deque;
  22. import java.util.HashMap;
  23. import java.util.Iterator;
  24. import java.util.LinkedList;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.logging.Level;
  28. import java.util.logging.Logger;
  29. import com.vaadin.server.Sizeable.Unit;
  30. import com.vaadin.ui.AbstractOrderedLayout;
  31. import com.vaadin.ui.AbstractSplitPanel;
  32. import com.vaadin.ui.Component;
  33. import com.vaadin.ui.ComponentContainer;
  34. import com.vaadin.ui.CustomComponent;
  35. import com.vaadin.ui.GridLayout;
  36. import com.vaadin.ui.GridLayout.Area;
  37. import com.vaadin.ui.HasComponents;
  38. import com.vaadin.ui.Panel;
  39. import com.vaadin.ui.TabSheet;
  40. import com.vaadin.ui.UI;
  41. import com.vaadin.ui.VerticalLayout;
  42. import com.vaadin.ui.Window;
  43. @SuppressWarnings({ "serial", "deprecation" })
  44. public class ComponentSizeValidator implements Serializable {
  45. private static final int LAYERS_SHOWN = 4;
  46. /**
  47. * Recursively checks given component and its subtree for invalid layout
  48. * setups. Prints errors to std err stream.
  49. *
  50. * @param component
  51. * component to check
  52. * @return set of first level errors found
  53. */
  54. public static List<InvalidLayout> validateComponentRelativeSizes(
  55. Component component, List<InvalidLayout> errors,
  56. InvalidLayout parent) {
  57. if (component != null) {
  58. boolean invalidHeight = !checkHeights(component);
  59. boolean invalidWidth = !checkWidths(component);
  60. if (invalidHeight || invalidWidth) {
  61. InvalidLayout error = new InvalidLayout(component,
  62. invalidHeight, invalidWidth);
  63. if (parent != null) {
  64. parent.addError(error);
  65. } else {
  66. if (errors == null) {
  67. errors = new LinkedList<>();
  68. }
  69. errors.add(error);
  70. }
  71. parent = error;
  72. }
  73. }
  74. if (component instanceof Panel) {
  75. Panel panel = (Panel) component;
  76. errors = validateComponentRelativeSizes(panel.getContent(), errors,
  77. parent);
  78. } else if (component instanceof ComponentContainer) {
  79. ComponentContainer lo = (ComponentContainer) component;
  80. Iterator<Component> it = lo.getComponentIterator();
  81. while (it.hasNext()) {
  82. errors = validateComponentRelativeSizes(it.next(), errors,
  83. parent);
  84. }
  85. } else if (isForm(component)) {
  86. HasComponents form = (HasComponents) component;
  87. for (Component child : form) {
  88. errors = validateComponentRelativeSizes(child, errors, parent);
  89. }
  90. }
  91. return errors;
  92. }
  93. /**
  94. * Comparability form component which is defined in the different jar.
  95. *
  96. * TODO : Normally this logic shouldn't be here. But it means that the whole
  97. * this class has wrong design and implementation and should be refactored.
  98. */
  99. private static boolean isForm(Component component) {
  100. if (!(component instanceof HasComponents)) {
  101. return false;
  102. }
  103. Class<?> clazz = component.getClass();
  104. while (clazz != null) {
  105. if (component.getClass().getName()
  106. .equals("com.vaadin.v7.ui.Form")) {
  107. return true;
  108. }
  109. clazz = clazz.getSuperclass();
  110. }
  111. return false;
  112. }
  113. private static void printServerError(String msg,
  114. Deque<ComponentInfo> attributes, boolean widthError,
  115. PrintStream errorStream) {
  116. StringBuilder err = new StringBuilder();
  117. err.append("Vaadin DEBUG\n");
  118. StringBuilder indent = new StringBuilder();
  119. ComponentInfo ci;
  120. if (attributes != null) {
  121. while (attributes.size() > LAYERS_SHOWN) {
  122. attributes.pop();
  123. }
  124. while (!attributes.isEmpty()) {
  125. ci = attributes.pop();
  126. showComponent(ci.component, ci.info, err, indent, widthError);
  127. }
  128. }
  129. err.append("Layout problem detected: ");
  130. err.append(msg);
  131. err.append("\n");
  132. err.append(
  133. "Relative sizes were replaced by undefined sizes, components may not render as expected.\n");
  134. errorStream.println(err);
  135. }
  136. public static boolean checkHeights(Component component) {
  137. try {
  138. if (!hasRelativeHeight(component)) {
  139. return true;
  140. }
  141. if (component instanceof Window) {
  142. return true;
  143. }
  144. if (component.getParent() == null) {
  145. return true;
  146. }
  147. return parentCanDefineHeight(component);
  148. } catch (Exception e) {
  149. getLogger().log(Level.FINER,
  150. "An exception occurred while validating sizes.", e);
  151. return true;
  152. }
  153. }
  154. public static boolean checkWidths(Component component) {
  155. try {
  156. if (!hasRelativeWidth(component)) {
  157. return true;
  158. }
  159. if (component instanceof Window) {
  160. return true;
  161. }
  162. if (component.getParent() == null) {
  163. return true;
  164. }
  165. return parentCanDefineWidth(component);
  166. } catch (Exception e) {
  167. getLogger().log(Level.FINER,
  168. "An exception occurred while validating sizes.", e);
  169. return true;
  170. }
  171. }
  172. public static class InvalidLayout implements Serializable {
  173. private final Component component;
  174. private final boolean invalidHeight;
  175. private final boolean invalidWidth;
  176. private final List<InvalidLayout> subErrors = new ArrayList<>();
  177. public InvalidLayout(Component component, boolean height,
  178. boolean width) {
  179. this.component = component;
  180. invalidHeight = height;
  181. invalidWidth = width;
  182. }
  183. public void addError(InvalidLayout error) {
  184. subErrors.add(error);
  185. }
  186. public void reportErrors(StringBuilder clientJSON,
  187. PrintStream serverErrorStream) {
  188. clientJSON.append('{');
  189. Component parent = component.getParent();
  190. String paintableId = component.getConnectorId();
  191. clientJSON.append("\"id\":\"").append(paintableId).append("\"");
  192. if (invalidHeight) {
  193. Deque<ComponentInfo> attributes = null;
  194. String msg = "";
  195. // set proper error messages
  196. if (parent instanceof AbstractOrderedLayout) {
  197. AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
  198. boolean vertical = false;
  199. if (ol instanceof VerticalLayout) {
  200. vertical = true;
  201. }
  202. if (vertical) {
  203. msg = "Component with relative height inside a VerticalLayout with no height defined.";
  204. attributes = getHeightAttributes(component);
  205. } else {
  206. msg = "At least one of a HorizontalLayout's components must have non relative height if the height of the layout is not defined";
  207. attributes = getHeightAttributes(component);
  208. }
  209. } else if (parent instanceof GridLayout) {
  210. msg = "At least one of the GridLayout's components in each row should have non relative height if the height of the layout is not defined.";
  211. attributes = getHeightAttributes(component);
  212. } else {
  213. // default error for non sized parent issue
  214. msg = "A component with relative height needs a parent with defined height.";
  215. attributes = getHeightAttributes(component);
  216. }
  217. printServerError(msg, attributes, false, serverErrorStream);
  218. clientJSON.append(",\"heightMsg\":\"").append(msg).append("\"");
  219. }
  220. if (invalidWidth) {
  221. Deque<ComponentInfo> attributes = null;
  222. String msg = "";
  223. if (parent instanceof AbstractOrderedLayout) {
  224. AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
  225. boolean horizontal = true;
  226. if (ol instanceof VerticalLayout) {
  227. horizontal = false;
  228. }
  229. if (horizontal) {
  230. msg = "Component with relative width inside a HorizontalLayout with no width defined";
  231. attributes = getWidthAttributes(component);
  232. } else {
  233. msg = "At least one of a VerticalLayout's components must have non relative width if the width of the layout is not defined";
  234. attributes = getWidthAttributes(component);
  235. }
  236. } else if (parent instanceof GridLayout) {
  237. msg = "At least one of the GridLayout's components in each column should have non relative width if the width of the layout is not defined.";
  238. attributes = getWidthAttributes(component);
  239. } else {
  240. // default error for non sized parent issue
  241. msg = "A component with relative width needs a parent with defined width.";
  242. attributes = getWidthAttributes(component);
  243. }
  244. clientJSON.append(",\"widthMsg\":\"").append(msg).append("\"");
  245. printServerError(msg, attributes, true, serverErrorStream);
  246. }
  247. if (!subErrors.isEmpty()) {
  248. serverErrorStream.println("Sub errors >>");
  249. clientJSON.append(", \"subErrors\" : [");
  250. boolean first = true;
  251. for (InvalidLayout subError : subErrors) {
  252. if (!first) {
  253. clientJSON.append(',');
  254. } else {
  255. first = false;
  256. }
  257. subError.reportErrors(clientJSON, serverErrorStream);
  258. }
  259. clientJSON.append(']');
  260. serverErrorStream.println("<< Sub erros");
  261. }
  262. clientJSON.append('}');
  263. }
  264. }
  265. private static class ComponentInfo implements Serializable {
  266. Component component;
  267. String info;
  268. public ComponentInfo(Component component, String info) {
  269. this.component = component;
  270. this.info = info;
  271. }
  272. }
  273. private static Deque<ComponentInfo> getHeightAttributes(
  274. Component component) {
  275. Deque<ComponentInfo> attributes = new ArrayDeque<>();
  276. attributes
  277. .add(new ComponentInfo(component, getHeightString(component)));
  278. Component parent = component.getParent();
  279. attributes.add(new ComponentInfo(parent, getHeightString(parent)));
  280. while ((parent = parent.getParent()) != null) {
  281. attributes.add(new ComponentInfo(parent, getHeightString(parent)));
  282. }
  283. return attributes;
  284. }
  285. private static Deque<ComponentInfo> getWidthAttributes(
  286. Component component) {
  287. final Deque<ComponentInfo> attributes = new ArrayDeque<>();
  288. attributes.add(new ComponentInfo(component, getWidthString(component)));
  289. Component parent = component.getParent();
  290. attributes.add(new ComponentInfo(parent, getWidthString(parent)));
  291. while ((parent = parent.getParent()) != null) {
  292. attributes.add(new ComponentInfo(parent, getWidthString(parent)));
  293. }
  294. return attributes;
  295. }
  296. private static String getWidthString(Component component) {
  297. String width = "width: ";
  298. if (hasRelativeWidth(component)) {
  299. width += "RELATIVE, " + component.getWidth() + " %";
  300. } else if (component instanceof Window
  301. && component.getParent() == null) {
  302. width += "MAIN WINDOW";
  303. } else if (component.getWidth() >= 0) {
  304. width += "ABSOLUTE, " + component.getWidth() + " "
  305. + component.getWidthUnits().getSymbol();
  306. } else {
  307. width += "UNDEFINED";
  308. }
  309. return width;
  310. }
  311. private static String getHeightString(Component component) {
  312. String height = "height: ";
  313. if (hasRelativeHeight(component)) {
  314. height += "RELATIVE, " + component.getHeight() + " %";
  315. } else if (component instanceof Window
  316. && component.getParent() == null) {
  317. height += "MAIN WINDOW";
  318. } else if (component.getHeight() > 0) {
  319. height += "ABSOLUTE, " + component.getHeight() + " "
  320. + component.getHeightUnits().getSymbol();
  321. } else {
  322. height += "UNDEFINED";
  323. }
  324. return height;
  325. }
  326. private static void showComponent(Component component, String attribute,
  327. StringBuilder err, StringBuilder indent, boolean widthError) {
  328. FileLocation createLoc = CREATION_LOCATIONS.get(component);
  329. FileLocation sizeLoc;
  330. if (widthError) {
  331. sizeLoc = WIDTH_LOCATIONS.get(component);
  332. } else {
  333. sizeLoc = HEIGHT_LOCATIONS.get(component);
  334. }
  335. err.append(indent);
  336. indent.append(" ");
  337. err.append("- ");
  338. err.append(component.getClass().getSimpleName());
  339. err.append('/').append(Integer.toHexString(component.hashCode()));
  340. if (component.getCaption() != null) {
  341. err.append(" \"");
  342. err.append(component.getCaption());
  343. err.append("\"");
  344. }
  345. if (component.getId() != null) {
  346. err.append(" id: ");
  347. err.append(component.getId());
  348. }
  349. if (createLoc != null) {
  350. err.append(", created at (").append(createLoc.file).append(':')
  351. .append(createLoc.lineNumber).append(')');
  352. }
  353. if (attribute != null) {
  354. err.append(" (");
  355. err.append(attribute);
  356. if (sizeLoc != null) {
  357. err.append(", set at (").append(sizeLoc.file).append(':')
  358. .append(sizeLoc.lineNumber).append(')');
  359. }
  360. err.append(')');
  361. }
  362. err.append("\n");
  363. }
  364. private static boolean hasNonRelativeHeightComponent(
  365. AbstractOrderedLayout ol) {
  366. Iterator<Component> it = ol.getComponentIterator();
  367. while (it.hasNext()) {
  368. if (!hasRelativeHeight(it.next())) {
  369. return true;
  370. }
  371. }
  372. return false;
  373. }
  374. public static boolean parentCanDefineHeight(Component component) {
  375. Component parent = component.getParent();
  376. if (parent == null) {
  377. // main window
  378. return true;
  379. }
  380. if (parent.getHeight() < 0) {
  381. // Undefined height
  382. if (parent instanceof Window) {
  383. // Sub window with undefined size has a min-height
  384. return true;
  385. }
  386. if (parent instanceof AbstractOrderedLayout) {
  387. if (parent instanceof VerticalLayout) {
  388. return false;
  389. }
  390. return hasNonRelativeHeightComponent(
  391. (AbstractOrderedLayout) parent);
  392. } else if (parent instanceof GridLayout) {
  393. GridLayout gl = (GridLayout) parent;
  394. Area componentArea = gl.getComponentArea(component);
  395. for (int row = componentArea.getRow1(); row <= componentArea
  396. .getRow2(); row++) {
  397. for (int column = 0; column < gl.getColumns(); column++) {
  398. Component c = gl.getComponent(column, row);
  399. if (c != null) {
  400. if (!hasRelativeHeight(c)) {
  401. return true;
  402. }
  403. }
  404. }
  405. }
  406. return false;
  407. } else if (isForm(parent)) {
  408. /*
  409. * If some other part of the form is not relative it determines
  410. * the component width
  411. */
  412. return formHasNonRelativeWidthComponent(parent);
  413. }
  414. if (parent instanceof Panel || parent instanceof AbstractSplitPanel
  415. || parent instanceof TabSheet
  416. || parent instanceof CustomComponent) {
  417. // height undefined, we know how how component works and no
  418. // exceptions
  419. // TODO horiz SplitPanel ??
  420. return false;
  421. } else {
  422. // We cannot generally know if undefined component can serve
  423. // space for children (like CustomLayout or component built by
  424. // third party) so we assume they can
  425. return true;
  426. }
  427. } else if (hasRelativeHeight(parent)) {
  428. // Relative height
  429. if (parent.getParent() != null) {
  430. return parentCanDefineHeight(parent);
  431. }
  432. return true;
  433. } else {
  434. // Absolute height
  435. return true;
  436. }
  437. }
  438. /**
  439. * Comparability form component which is defined in the different jar.
  440. *
  441. * TODO : Normally this logic shouldn't be here. But it means that the whole
  442. * this class has wrong design and implementation and should be refactored.
  443. */
  444. private static boolean formHasNonRelativeWidthComponent(Component form) {
  445. HasComponents parent = (HasComponents) form;
  446. for (Component aParent : parent) {
  447. if (!hasRelativeWidth(aParent)) {
  448. return true;
  449. }
  450. }
  451. return false;
  452. }
  453. private static boolean hasRelativeHeight(Component component) {
  454. return (component.getHeightUnits() == Unit.PERCENTAGE
  455. && component.getHeight() > 0);
  456. }
  457. private static boolean hasNonRelativeWidthComponent(
  458. AbstractOrderedLayout ol) {
  459. Iterator<Component> it = ol.getComponentIterator();
  460. while (it.hasNext()) {
  461. if (!hasRelativeWidth(it.next())) {
  462. return true;
  463. }
  464. }
  465. return false;
  466. }
  467. private static boolean hasRelativeWidth(Component paintable) {
  468. return paintable.getWidth() > 0
  469. && paintable.getWidthUnits() == Unit.PERCENTAGE;
  470. }
  471. public static boolean parentCanDefineWidth(Component component) {
  472. Component parent = component.getParent();
  473. if (parent == null) {
  474. // main window
  475. return true;
  476. }
  477. if (parent instanceof Window) {
  478. // Sub window with undefined size has a min-width
  479. return true;
  480. }
  481. if (parent.getWidth() < 0) {
  482. // Undefined width
  483. if (parent instanceof AbstractOrderedLayout) {
  484. AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
  485. // VerticalLayout and a child defines height
  486. return ol instanceof VerticalLayout
  487. && hasNonRelativeWidthComponent(ol);
  488. } else if (parent instanceof GridLayout) {
  489. GridLayout gl = (GridLayout) parent;
  490. Area componentArea = gl.getComponentArea(component);
  491. for (int col = componentArea.getColumn1(); col <= componentArea
  492. .getColumn2(); col++) {
  493. for (int row = 0; row < gl.getRows(); row++) {
  494. Component c = gl.getComponent(col, row);
  495. if (c != null) {
  496. if (!hasRelativeWidth(c)) {
  497. return true;
  498. }
  499. }
  500. }
  501. }
  502. return false;
  503. } else if (parent instanceof AbstractSplitPanel
  504. || parent instanceof TabSheet
  505. || parent instanceof CustomComponent) {
  506. // FIXME Could we use com.vaadin package name here and
  507. // fail for all component containers?
  508. // FIXME Actually this should be moved to containers so it can
  509. // be implemented for custom containers
  510. // TODO vertical splitpanel with another non relative component?
  511. return false;
  512. } else if (parent instanceof Window) {
  513. // Sub window can define width based on caption
  514. return parent.getCaption() != null
  515. && !parent.getCaption().isEmpty();
  516. }
  517. // TODO Panel should be able to define width based on caption
  518. return !(parent instanceof Panel);
  519. } else if (hasRelativeWidth(parent)) {
  520. // Relative width
  521. if (parent.getParent() == null) {
  522. return true;
  523. }
  524. return parentCanDefineWidth(parent);
  525. } else {
  526. return true;
  527. }
  528. }
  529. private static final Map<Object, FileLocation> CREATION_LOCATIONS = new HashMap<>();
  530. private static final Map<Object, FileLocation> WIDTH_LOCATIONS = new HashMap<>();
  531. private static final Map<Object, FileLocation> HEIGHT_LOCATIONS = new HashMap<>();
  532. public static class FileLocation implements Serializable {
  533. public String method;
  534. public String file;
  535. public String className;
  536. public String classNameSimple;
  537. public int lineNumber;
  538. public FileLocation(StackTraceElement traceElement) {
  539. file = traceElement.getFileName();
  540. className = traceElement.getClassName();
  541. classNameSimple = className
  542. .substring(className.lastIndexOf('.') + 1);
  543. lineNumber = traceElement.getLineNumber();
  544. method = traceElement.getMethodName();
  545. }
  546. }
  547. public static void setCreationLocation(Object object) {
  548. setLocation(CREATION_LOCATIONS, object);
  549. }
  550. public static void setWidthLocation(Object object) {
  551. setLocation(WIDTH_LOCATIONS, object);
  552. }
  553. public static void setHeightLocation(Object object) {
  554. setLocation(HEIGHT_LOCATIONS, object);
  555. }
  556. private static void setLocation(Map<Object, FileLocation> map,
  557. Object object) {
  558. StackTraceElement[] traceLines = Thread.currentThread().getStackTrace();
  559. for (StackTraceElement traceElement : traceLines) {
  560. Class<?> cls;
  561. try {
  562. String className = traceElement.getClassName();
  563. if (className.startsWith("java.")
  564. || className.startsWith("sun.")) {
  565. continue;
  566. }
  567. cls = Class.forName(className);
  568. if (cls == ComponentSizeValidator.class
  569. || cls == Thread.class) {
  570. continue;
  571. }
  572. if (Component.class.isAssignableFrom(cls)
  573. && !CustomComponent.class.isAssignableFrom(cls)) {
  574. continue;
  575. }
  576. FileLocation cl = new FileLocation(traceElement);
  577. map.put(object, cl);
  578. return;
  579. } catch (Exception e) {
  580. getLogger().log(Level.FINER,
  581. "An exception occurred while validating sizes.", e);
  582. }
  583. }
  584. }
  585. private static Logger getLogger() {
  586. return Logger.getLogger(ComponentSizeValidator.class.getName());
  587. }
  588. /**
  589. * Validates the layout and returns a collection of errors.
  590. *
  591. * @since 7.1
  592. * @param ui
  593. * The UI to validate
  594. * @return A collection of errors. An empty collection if there are no
  595. * errors.
  596. */
  597. public static List<InvalidLayout> validateLayouts(UI ui) {
  598. List<InvalidLayout> invalidRelativeSizes = ComponentSizeValidator
  599. .validateComponentRelativeSizes(ui.getContent(),
  600. new ArrayList<>(), null);
  601. // Also check any existing subwindows
  602. if (ui.getWindows() != null) {
  603. for (Window subWindow : ui.getWindows()) {
  604. invalidRelativeSizes = ComponentSizeValidator
  605. .validateComponentRelativeSizes(subWindow.getContent(),
  606. invalidRelativeSizes, null);
  607. }
  608. }
  609. return invalidRelativeSizes;
  610. }
  611. private ComponentSizeValidator() {
  612. }
  613. }