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 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. /*
  2. * Copyright 2011 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.PrintWriter;
  19. import java.io.Serializable;
  20. import java.util.HashMap;
  21. import java.util.Iterator;
  22. import java.util.LinkedList;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Stack;
  26. import java.util.Vector;
  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.Form;
  36. import com.vaadin.ui.GridLayout;
  37. import com.vaadin.ui.GridLayout.Area;
  38. import com.vaadin.ui.Layout;
  39. import com.vaadin.ui.Panel;
  40. import com.vaadin.ui.TabSheet;
  41. import com.vaadin.ui.VerticalLayout;
  42. import com.vaadin.ui.Window;
  43. @SuppressWarnings({ "serial", "deprecation" })
  44. public class ComponentSizeValidator implements Serializable {
  45. private final static 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. boolean invalidHeight = !checkHeights(component);
  58. boolean invalidWidth = !checkWidths(component);
  59. if (invalidHeight || invalidWidth) {
  60. InvalidLayout error = new InvalidLayout(component, invalidHeight,
  61. invalidWidth);
  62. if (parent != null) {
  63. parent.addError(error);
  64. } else {
  65. if (errors == null) {
  66. errors = new LinkedList<InvalidLayout>();
  67. }
  68. errors.add(error);
  69. }
  70. parent = error;
  71. }
  72. if (component instanceof Panel) {
  73. Panel panel = (Panel) component;
  74. errors = validateComponentRelativeSizes(panel.getContent(), errors,
  75. parent);
  76. } else if (component instanceof ComponentContainer) {
  77. ComponentContainer lo = (ComponentContainer) component;
  78. Iterator<Component> it = lo.getComponentIterator();
  79. while (it.hasNext()) {
  80. errors = validateComponentRelativeSizes(it.next(), errors,
  81. parent);
  82. }
  83. } else if (component instanceof Form) {
  84. Form form = (Form) component;
  85. if (form.getLayout() != null) {
  86. errors = validateComponentRelativeSizes(form.getLayout(),
  87. errors, parent);
  88. }
  89. if (form.getFooter() != null) {
  90. errors = validateComponentRelativeSizes(form.getFooter(),
  91. errors, parent);
  92. }
  93. }
  94. return errors;
  95. }
  96. private static void printServerError(String msg,
  97. Stack<ComponentInfo> attributes, boolean widthError,
  98. PrintStream errorStream) {
  99. StringBuffer err = new StringBuffer();
  100. err.append("Vaadin DEBUG\n");
  101. StringBuilder indent = new StringBuilder("");
  102. ComponentInfo ci;
  103. if (attributes != null) {
  104. while (attributes.size() > LAYERS_SHOWN) {
  105. attributes.pop();
  106. }
  107. while (!attributes.empty()) {
  108. ci = attributes.pop();
  109. showComponent(ci.component, ci.info, err, indent, widthError);
  110. }
  111. }
  112. err.append("Layout problem detected: ");
  113. err.append(msg);
  114. err.append("\n");
  115. err.append("Relative sizes were replaced by undefined sizes, components may not render as expected.\n");
  116. errorStream.println(err);
  117. }
  118. public static boolean checkHeights(Component component) {
  119. try {
  120. if (!hasRelativeHeight(component)) {
  121. return true;
  122. }
  123. if (component instanceof Window) {
  124. return true;
  125. }
  126. if (component.getParent() == null) {
  127. return true;
  128. }
  129. return parentCanDefineHeight(component);
  130. } catch (Exception e) {
  131. getLogger().log(Level.FINER,
  132. "An exception occurred while validating sizes.", e);
  133. return true;
  134. }
  135. }
  136. public static boolean checkWidths(Component component) {
  137. try {
  138. if (!hasRelativeWidth(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 parentCanDefineWidth(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 class InvalidLayout implements Serializable {
  155. private final Component component;
  156. private final boolean invalidHeight;
  157. private final boolean invalidWidth;
  158. private final Vector<InvalidLayout> subErrors = new Vector<InvalidLayout>();
  159. public InvalidLayout(Component component, boolean height, boolean width) {
  160. this.component = component;
  161. invalidHeight = height;
  162. invalidWidth = width;
  163. }
  164. public void addError(InvalidLayout error) {
  165. subErrors.add(error);
  166. }
  167. public void reportErrors(PrintWriter clientJSON,
  168. AbstractCommunicationManager communicationManager,
  169. PrintStream serverErrorStream) {
  170. clientJSON.write("{");
  171. Component parent = component.getParent();
  172. String paintableId = component.getConnectorId();
  173. clientJSON.print("id:\"" + paintableId + "\"");
  174. if (invalidHeight) {
  175. Stack<ComponentInfo> attributes = null;
  176. String msg = "";
  177. // set proper error messages
  178. if (parent instanceof AbstractOrderedLayout) {
  179. AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
  180. boolean vertical = false;
  181. if (ol instanceof VerticalLayout) {
  182. vertical = true;
  183. }
  184. if (vertical) {
  185. msg = "Component with relative height inside a VerticalLayout with no height defined.";
  186. attributes = getHeightAttributes(component);
  187. } else {
  188. msg = "At least one of a HorizontalLayout's components must have non relative height if the height of the layout is not defined";
  189. attributes = getHeightAttributes(component);
  190. }
  191. } else if (parent instanceof GridLayout) {
  192. 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.";
  193. attributes = getHeightAttributes(component);
  194. } else {
  195. // default error for non sized parent issue
  196. msg = "A component with relative height needs a parent with defined height.";
  197. attributes = getHeightAttributes(component);
  198. }
  199. printServerError(msg, attributes, false, serverErrorStream);
  200. clientJSON.print(",\"heightMsg\":\"" + msg + "\"");
  201. }
  202. if (invalidWidth) {
  203. Stack<ComponentInfo> attributes = null;
  204. String msg = "";
  205. if (parent instanceof AbstractOrderedLayout) {
  206. AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
  207. boolean horizontal = true;
  208. if (ol instanceof VerticalLayout) {
  209. horizontal = false;
  210. }
  211. if (horizontal) {
  212. msg = "Component with relative width inside a HorizontalLayout with no width defined";
  213. attributes = getWidthAttributes(component);
  214. } else {
  215. msg = "At least one of a VerticalLayout's components must have non relative width if the width of the layout is not defined";
  216. attributes = getWidthAttributes(component);
  217. }
  218. } else if (parent instanceof GridLayout) {
  219. 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.";
  220. attributes = getWidthAttributes(component);
  221. } else {
  222. // default error for non sized parent issue
  223. msg = "A component with relative width needs a parent with defined width.";
  224. attributes = getWidthAttributes(component);
  225. }
  226. clientJSON.print(",\"widthMsg\":\"" + msg + "\"");
  227. printServerError(msg, attributes, true, serverErrorStream);
  228. }
  229. if (subErrors.size() > 0) {
  230. serverErrorStream.println("Sub errors >>");
  231. clientJSON.write(", \"subErrors\" : [");
  232. boolean first = true;
  233. for (InvalidLayout subError : subErrors) {
  234. if (!first) {
  235. clientJSON.print(",");
  236. } else {
  237. first = false;
  238. }
  239. subError.reportErrors(clientJSON, communicationManager,
  240. serverErrorStream);
  241. }
  242. clientJSON.write("]");
  243. serverErrorStream.println("<< Sub erros");
  244. }
  245. clientJSON.write("}");
  246. }
  247. }
  248. private static class ComponentInfo implements Serializable {
  249. Component component;
  250. String info;
  251. public ComponentInfo(Component component, String info) {
  252. this.component = component;
  253. this.info = info;
  254. }
  255. }
  256. private static Stack<ComponentInfo> getHeightAttributes(Component component) {
  257. Stack<ComponentInfo> attributes = new Stack<ComponentInfo>();
  258. attributes
  259. .add(new ComponentInfo(component, getHeightString(component)));
  260. Component parent = component.getParent();
  261. attributes.add(new ComponentInfo(parent, getHeightString(parent)));
  262. while ((parent = parent.getParent()) != null) {
  263. attributes.add(new ComponentInfo(parent, getHeightString(parent)));
  264. }
  265. return attributes;
  266. }
  267. private static Stack<ComponentInfo> getWidthAttributes(Component component) {
  268. Stack<ComponentInfo> attributes = new Stack<ComponentInfo>();
  269. attributes.add(new ComponentInfo(component, getWidthString(component)));
  270. Component parent = component.getParent();
  271. attributes.add(new ComponentInfo(parent, getWidthString(parent)));
  272. while ((parent = parent.getParent()) != null) {
  273. attributes.add(new ComponentInfo(parent, getWidthString(parent)));
  274. }
  275. return attributes;
  276. }
  277. private static String getWidthString(Component component) {
  278. String width = "width: ";
  279. if (hasRelativeWidth(component)) {
  280. width += "RELATIVE, " + component.getWidth() + " %";
  281. } else if (component instanceof Window && component.getParent() == null) {
  282. width += "MAIN WINDOW";
  283. } else if (component.getWidth() >= 0) {
  284. width += "ABSOLUTE, " + component.getWidth() + " "
  285. + component.getWidthUnits().getSymbol();
  286. } else {
  287. width += "UNDEFINED";
  288. }
  289. return width;
  290. }
  291. private static String getHeightString(Component component) {
  292. String height = "height: ";
  293. if (hasRelativeHeight(component)) {
  294. height += "RELATIVE, " + component.getHeight() + " %";
  295. } else if (component instanceof Window && component.getParent() == null) {
  296. height += "MAIN WINDOW";
  297. } else if (component.getHeight() > 0) {
  298. height += "ABSOLUTE, " + component.getHeight() + " "
  299. + component.getHeightUnits().getSymbol();
  300. } else {
  301. height += "UNDEFINED";
  302. }
  303. return height;
  304. }
  305. private static void showComponent(Component component, String attribute,
  306. StringBuffer err, StringBuilder indent, boolean widthError) {
  307. FileLocation createLoc = creationLocations.get(component);
  308. FileLocation sizeLoc;
  309. if (widthError) {
  310. sizeLoc = widthLocations.get(component);
  311. } else {
  312. sizeLoc = heightLocations.get(component);
  313. }
  314. err.append(indent);
  315. indent.append(" ");
  316. err.append("- ");
  317. err.append(component.getClass().getSimpleName());
  318. err.append("/").append(Integer.toHexString(component.hashCode()));
  319. if (component.getCaption() != null) {
  320. err.append(" \"");
  321. err.append(component.getCaption());
  322. err.append("\"");
  323. }
  324. if (component.getId() != null) {
  325. err.append(" id: ");
  326. err.append(component.getId());
  327. }
  328. if (createLoc != null) {
  329. err.append(", created at (" + createLoc.file + ":"
  330. + createLoc.lineNumber + ")");
  331. }
  332. if (attribute != null) {
  333. err.append(" (");
  334. err.append(attribute);
  335. if (sizeLoc != null) {
  336. err.append(", set at (" + sizeLoc.file + ":"
  337. + sizeLoc.lineNumber + ")");
  338. }
  339. err.append(")");
  340. }
  341. err.append("\n");
  342. }
  343. private static boolean hasNonRelativeHeightComponent(
  344. AbstractOrderedLayout ol) {
  345. Iterator<Component> it = ol.getComponentIterator();
  346. while (it.hasNext()) {
  347. if (!hasRelativeHeight(it.next())) {
  348. return true;
  349. }
  350. }
  351. return false;
  352. }
  353. public static boolean parentCanDefineHeight(Component component) {
  354. Component parent = component.getParent();
  355. if (parent == null) {
  356. // main window, valid situation
  357. return true;
  358. }
  359. if (parent.getHeight() < 0) {
  360. // Undefined height
  361. if (parent instanceof Window) {
  362. // Sub window with undefined size has a min-height
  363. return true;
  364. }
  365. if (parent instanceof AbstractOrderedLayout) {
  366. boolean horizontal = true;
  367. if (parent instanceof VerticalLayout) {
  368. horizontal = false;
  369. }
  370. if (horizontal
  371. && hasNonRelativeHeightComponent((AbstractOrderedLayout) parent)) {
  372. return true;
  373. } else {
  374. return false;
  375. }
  376. } else if (parent instanceof GridLayout) {
  377. GridLayout gl = (GridLayout) parent;
  378. Area componentArea = gl.getComponentArea(component);
  379. boolean rowHasHeight = false;
  380. for (int row = componentArea.getRow1(); !rowHasHeight
  381. && row <= componentArea.getRow2(); row++) {
  382. for (int column = 0; !rowHasHeight
  383. && column < gl.getColumns(); column++) {
  384. Component c = gl.getComponent(column, row);
  385. if (c != null) {
  386. rowHasHeight = !hasRelativeHeight(c);
  387. }
  388. }
  389. }
  390. if (!rowHasHeight) {
  391. return false;
  392. } else {
  393. // Other components define row height
  394. return true;
  395. }
  396. }
  397. if (parent instanceof Panel || parent instanceof AbstractSplitPanel
  398. || parent instanceof TabSheet
  399. || parent instanceof CustomComponent) {
  400. // height undefined, we know how how component works and no
  401. // exceptions
  402. // TODO horiz SplitPanel ??
  403. return false;
  404. } else {
  405. // We cannot generally know if undefined component can serve
  406. // space for children (like CustomLayout or component built by
  407. // third party) so we assume they can
  408. return true;
  409. }
  410. } else if (hasRelativeHeight(parent)) {
  411. // Relative height
  412. if (parent.getParent() != null) {
  413. return parentCanDefineHeight(parent);
  414. } else {
  415. return true;
  416. }
  417. } else {
  418. // Absolute height
  419. return true;
  420. }
  421. }
  422. private static boolean hasRelativeHeight(Component component) {
  423. return (component.getHeightUnits() == Unit.PERCENTAGE && component
  424. .getHeight() > 0);
  425. }
  426. private static boolean hasNonRelativeWidthComponent(AbstractOrderedLayout ol) {
  427. Iterator<Component> it = ol.getComponentIterator();
  428. while (it.hasNext()) {
  429. if (!hasRelativeWidth(it.next())) {
  430. return true;
  431. }
  432. }
  433. return false;
  434. }
  435. private static boolean hasRelativeWidth(Component paintable) {
  436. return paintable.getWidth() > 0
  437. && paintable.getWidthUnits() == Unit.PERCENTAGE;
  438. }
  439. public static boolean parentCanDefineWidth(Component component) {
  440. Component parent = component.getParent();
  441. if (parent == null) {
  442. // main window, valid situation
  443. return true;
  444. }
  445. if (parent instanceof Window) {
  446. // Sub window with undefined size has a min-width
  447. return true;
  448. }
  449. if (parent.getWidth() < 0) {
  450. // Undefined width
  451. if (parent instanceof AbstractOrderedLayout) {
  452. AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
  453. boolean horizontal = true;
  454. if (ol instanceof VerticalLayout) {
  455. horizontal = false;
  456. }
  457. if (!horizontal && hasNonRelativeWidthComponent(ol)) {
  458. // valid situation, other components defined width
  459. return true;
  460. } else {
  461. return false;
  462. }
  463. } else if (parent instanceof GridLayout) {
  464. GridLayout gl = (GridLayout) parent;
  465. Area componentArea = gl.getComponentArea(component);
  466. boolean columnHasWidth = false;
  467. for (int col = componentArea.getColumn1(); !columnHasWidth
  468. && col <= componentArea.getColumn2(); col++) {
  469. for (int row = 0; !columnHasWidth && row < gl.getRows(); row++) {
  470. Component c = gl.getComponent(col, row);
  471. if (c != null) {
  472. columnHasWidth = !hasRelativeWidth(c);
  473. }
  474. }
  475. }
  476. if (!columnHasWidth) {
  477. return false;
  478. } else {
  479. // Other components define column width
  480. return true;
  481. }
  482. } else if (parent instanceof Form) {
  483. /*
  484. * If some other part of the form is not relative it determines
  485. * the component width
  486. */
  487. return hasNonRelativeWidthComponent((Form) parent);
  488. } else if (parent instanceof AbstractSplitPanel
  489. || parent instanceof TabSheet
  490. || parent instanceof CustomComponent) {
  491. // FIXME Could we use com.vaadin package name here and
  492. // fail for all component containers?
  493. // FIXME Actually this should be moved to containers so it can
  494. // be implemented for custom containers
  495. // TODO vertical splitpanel with another non relative component?
  496. return false;
  497. } else if (parent instanceof Window) {
  498. // Sub window can define width based on caption
  499. if (parent.getCaption() != null
  500. && !parent.getCaption().equals("")) {
  501. return true;
  502. } else {
  503. return false;
  504. }
  505. } else if (parent instanceof Panel) {
  506. // TODO Panel should be able to define width based on caption
  507. return false;
  508. } else {
  509. return true;
  510. }
  511. } else if (hasRelativeWidth(parent)) {
  512. // Relative width
  513. if (parent.getParent() == null) {
  514. return true;
  515. }
  516. return parentCanDefineWidth(parent);
  517. } else {
  518. return true;
  519. }
  520. }
  521. private static boolean hasNonRelativeWidthComponent(Form form) {
  522. Layout layout = form.getLayout();
  523. Layout footer = form.getFooter();
  524. if (layout != null && !hasRelativeWidth(layout)) {
  525. return true;
  526. }
  527. if (footer != null && !hasRelativeWidth(footer)) {
  528. return true;
  529. }
  530. return false;
  531. }
  532. private static Map<Object, FileLocation> creationLocations = new HashMap<Object, FileLocation>();
  533. private static Map<Object, FileLocation> widthLocations = new HashMap<Object, FileLocation>();
  534. private static Map<Object, FileLocation> heightLocations = new HashMap<Object, FileLocation>();
  535. public static class FileLocation implements Serializable {
  536. public String method;
  537. public String file;
  538. public String className;
  539. public String classNameSimple;
  540. public int lineNumber;
  541. public FileLocation(StackTraceElement traceElement) {
  542. file = traceElement.getFileName();
  543. className = traceElement.getClassName();
  544. classNameSimple = className
  545. .substring(className.lastIndexOf('.') + 1);
  546. lineNumber = traceElement.getLineNumber();
  547. method = traceElement.getMethodName();
  548. }
  549. }
  550. public static void setCreationLocation(Object object) {
  551. setLocation(creationLocations, object);
  552. }
  553. public static void setWidthLocation(Object object) {
  554. setLocation(widthLocations, object);
  555. }
  556. public static void setHeightLocation(Object object) {
  557. setLocation(heightLocations, object);
  558. }
  559. private static void setLocation(Map<Object, FileLocation> map, Object object) {
  560. StackTraceElement[] traceLines = Thread.currentThread().getStackTrace();
  561. for (StackTraceElement traceElement : traceLines) {
  562. Class<?> cls;
  563. try {
  564. String className = traceElement.getClassName();
  565. if (className.startsWith("java.")
  566. || className.startsWith("sun.")) {
  567. continue;
  568. }
  569. cls = Class.forName(className);
  570. if (cls == ComponentSizeValidator.class || cls == Thread.class) {
  571. continue;
  572. }
  573. if (Component.class.isAssignableFrom(cls)
  574. && !CustomComponent.class.isAssignableFrom(cls)) {
  575. continue;
  576. }
  577. FileLocation cl = new FileLocation(traceElement);
  578. map.put(object, cl);
  579. return;
  580. } catch (Exception e) {
  581. // TODO Auto-generated catch block
  582. getLogger().log(Level.FINER,
  583. "An exception occurred while validating sizes.", e);
  584. }
  585. }
  586. }
  587. private static Logger getLogger() {
  588. return Logger.getLogger(ComponentSizeValidator.class.getName());
  589. }
  590. }