/*******************************************************************************
* Copyright (c) 2012 OpenLegacy Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* OpenLegacy Inc. - initial API and implementation
*******************************************************************************/
package org.openlegacy.terminal.layout.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openlegacy.definitions.page.support.SimplePageDefinition;
import org.openlegacy.definitions.page.support.SimplePagePartDefinition;
import org.openlegacy.definitions.page.support.SimplePagePartRowDefinition;
import org.openlegacy.layout.PageDefinition;
import org.openlegacy.layout.PagePartDefinition;
import org.openlegacy.layout.PagePartRowDefinition;
import org.openlegacy.terminal.PositionedPart;
import org.openlegacy.terminal.ScreenSize;
import org.openlegacy.terminal.TerminalPosition;
import org.openlegacy.terminal.definitions.ScreenEntityDefinition;
import org.openlegacy.terminal.definitions.ScreenFieldDefinition;
import org.openlegacy.terminal.definitions.ScreenPartEntityDefinition;
import org.openlegacy.terminal.definitions.ScreenTableDefinition;
import org.openlegacy.terminal.definitions.ScreenTableDefinition.ScreenColumnDefinition;
import org.openlegacy.terminal.layout.ScreenPageBuilder;
import org.openlegacy.terminal.support.TerminalPositionContainerComparator;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Default screen page builder create a page model which is combined from page parts. Looks for neighbor fields by row & column,
* and create page part with page part rows. Screen parts and tables are converted automatically to page parts
*
* @author Roi Mor
*
*/
public class DefaultScreenPageBuilder implements ScreenPageBuilder {
private final static Log logger = LogFactory.getLog(DefaultScreenPageBuilder.class);
private int maxRowDistanceWithinPart = 1;
private int maxColumnDistanceWithinPart = 40;
private int labelFieldDistance = 3;
// used for setting default field length when none specified (@ScreenField doesn't force endColumn/length)
private int defaultFieldLength = 10;
private int defaultLeftMarginOffset = 0;
private int defaultTopMarginOffset = 0;
private int additionalPartWidth = 0;
/**
* Determine the max distance between to fields start, to consider them in a single panel column
*/
private int maxNeighbourColumnsOffset = 5;
/**
* Page builder entry point. Builds a page definition from a screen entity definition
*/
public PageDefinition build(ScreenEntityDefinition entityDefinition) {
Collection<ScreenFieldDefinition> fields = entityDefinition.getFieldsDefinitions().values();
PageDefinition pageDefinition = new SimplePageDefinition(entityDefinition);
// copy all actions to the page definition to allow separate control in page actions
pageDefinition.getActions().addAll(entityDefinition.getActions());
// sort fields by position
List<ScreenFieldDefinition> sortedFields = new ArrayList<ScreenFieldDefinition>(fields);
Collections.sort(sortedFields, TerminalPositionContainerComparator.instance());
// create groups of neighbor fields
List<List<ScreenFieldDefinition>> neighourFieldsGroups = groupNeighbourFields(sortedFields);
// create a page part from each fields group
for (List<ScreenFieldDefinition> neighbourfields : neighourFieldsGroups) {
pageDefinition.getPageParts().add(buildPagePart(neighbourfields, entityDefinition));
}
// create page parts from screen parts
Collection<ScreenPartEntityDefinition> screenParts = entityDefinition.getPartsDefinitions().values();
for (ScreenPartEntityDefinition screenPartEntityDefinition : screenParts) {
pageDefinition.getPageParts().add(buildPagePartFromScreenPart(screenPartEntityDefinition, entityDefinition));
}
// create page parts from screen tables
Collection<String> tables = entityDefinition.getTableDefinitions().keySet();
for (String tableFieldName : tables) {
ScreenTableDefinition tableDefinition = entityDefinition.getTableDefinitions().get(tableFieldName);
pageDefinition.getPageParts().add(buildPagePartFromTable(tableFieldName, tableDefinition, entityDefinition));
}
return pageDefinition;
}
/**
* Build a page part from screen table. calculates the table top/left position and positions the page part by this position
*
* @param tableFieldName
* the table field name within the entity definition
* @param tableDefinition
* table definition which the page part should be based on
* @param entityDefinition
* entity definition which holds the table
* @return page part
*/
private PagePartDefinition buildPagePartFromTable(String tableFieldName, ScreenTableDefinition tableDefinition,
ScreenEntityDefinition entityDefinition) {
SimplePagePartDefinition pagePart = new SimplePagePartDefinition();
List<ScreenColumnDefinition> columns = tableDefinition.getColumnDefinitions();
int tableStartColumn = columns.get(0).getStartColumn();
int tableEndColumn = columns.get(columns.size() - 1).getEndColumn();
int firstRow = tableDefinition.getStartRow() - 1; // -1 -> header
ScreenSize screenSize = entityDefinition.getScreenSize();
// -1 -> since it is consider in HTML as start of the positioning
int topMarginPercentage = (100 * (firstRow - 1)) / screenSize.getRows();
int leftMarginPercentage = (100 * tableStartColumn) / screenSize.getColumns();
pagePart.setTopMargin(topMarginPercentage);
pagePart.setLeftMargin(leftMarginPercentage);
calculateWidth(entityDefinition, pagePart, tableEndColumn - tableStartColumn);
pagePart.setTableFieldName(tableFieldName);
pagePart.setTableDefinition(tableDefinition);
overridePositionAndWidth((PositionedPart)tableDefinition, entityDefinition, pagePart);
return pagePart;
}
private PagePartDefinition buildPagePartFromScreenPart(ScreenPartEntityDefinition screenPartEntityDefinition,
ScreenEntityDefinition entityDefinition) {
Collection<ScreenFieldDefinition> fields = screenPartEntityDefinition.getFieldsDefinitions().values();
List<ScreenFieldDefinition> sortedFields = new ArrayList<ScreenFieldDefinition>(fields);
Collections.sort(sortedFields, TerminalPositionContainerComparator.instance());
SimplePagePartDefinition pagePart = (SimplePagePartDefinition)buildPagePart(sortedFields, entityDefinition);
pagePart.setDisplayName(screenPartEntityDefinition.getDisplayName());
// if screen part has position (loaded from @PartPosition), override the calculated position
overridePositionAndWidth((PositionedPart)screenPartEntityDefinition, entityDefinition, pagePart);
return pagePart;
}
private void overridePositionAndWidth(PositionedPart positionedPart, ScreenEntityDefinition entityDefinition,
SimplePagePartDefinition pagePart) {
if (positionedPart.getPartPosition() != null) {
int leftMargin = calculateLeftMargin(entityDefinition.getScreenSize(), positionedPart.getPartPosition().getColumn());
pagePart.setLeftMargin(leftMargin);
int topMargin = calculateTopMargin(entityDefinition.getScreenSize(), positionedPart.getPartPosition().getRow());
pagePart.setTopMargin(topMargin);
}
if (positionedPart.getWidth() > 0) {
calculateWidth(entityDefinition, pagePart, positionedPart.getWidth());
// if has width (set from @PartPosition) but no position - set it as relative
if (positionedPart.getPartPosition() == null) {
pagePart.setRelative(true);
}
}
}
/**
* Builds a page part from a list of neighbor fields
*
* @param fields
* the neighbor fields
* @param entityDefinition
* the entity containing all the fields
* @return page part
*/
private PagePartDefinition buildPagePart(List<ScreenFieldDefinition> fields, ScreenEntityDefinition entityDefinition) {
SimplePagePartDefinition pagePart = new SimplePagePartDefinition();
int currentRow = -1;
PagePartRowDefinition currentPagePartRow = null;
Set<Integer> columnValues = new HashSet<Integer>();
if (fields.size() == 0) {
logger.warn("A screen/screen part with 0 fields found. Page part not created. Class:"
+ entityDefinition.getEntityClassName());
return pagePart;
}
ScreenFieldDefinition firstField = fields.get(0);
int startColumn = calculateStartColumn(firstField);
int endColumn = calculateEndColumn(firstField);
// iterate through all the neighbor fields, and build row part rows upon row change, and find the end column
for (ScreenFieldDefinition screenFieldDefinition : fields) {
if (screenFieldDefinition.getPosition().getRow() != currentRow) {
currentPagePartRow = new SimplePagePartRowDefinition();
pagePart.getPartRows().add(currentPagePartRow);
currentRow = screenFieldDefinition.getPosition().getRow();
}
currentPagePartRow.getFields().add(screenFieldDefinition);
int fieldStartColumn = calculateStartColumn(screenFieldDefinition);
int fieldEndColumn = calculateEndColumn(screenFieldDefinition);
columnValues.add(getFieldLogicalStart(fieldStartColumn, fieldEndColumn));
// find the most right end column
if (fieldEndColumn > endColumn) {
endColumn = fieldEndColumn;
}
if (fieldStartColumn < startColumn) {
startColumn = fieldStartColumn;
}
}
columnValues = calculateNumberOfColumnsForPagePage(columnValues);
pagePart.setColumns(columnValues.size());
calculatePartPosition(fields, entityDefinition, pagePart, startColumn);
calculateWidth(entityDefinition, pagePart, endColumn - startColumn);
return pagePart;
}
protected Integer getFieldLogicalStart(int fieldStartColumn, int fieldEndColumn) {
return fieldStartColumn;
}
/**
* Calculate the row part columns consider small offsets between neighbor fields. e.g: Field which starts at column 10 and
* field which starts at columns 12, will be considered as 1
*
* @param columnValues
* @return
*/
private Set<Integer> calculateNumberOfColumnsForPagePage(Set<Integer> columnValues) {
Integer[] columnValuesArr = columnValues.toArray(new Integer[columnValues.size()]);
// make sure columns are ordered
Arrays.sort(columnValuesArr);
Set<Integer> newColumnValues = new HashSet<Integer>();
newColumnValues.add(columnValuesArr[0]);
for (int i = 0; i < columnValuesArr.length - 1; i++) {
int columnDistance = Math.abs(columnValuesArr[i] - columnValuesArr[i + 1]);
if (columnDistance > maxNeighbourColumnsOffset) {
newColumnValues.add(columnValuesArr[i + 1]);
}
}
return newColumnValues;
}
/**
* Calculated the given page part width in percentages. Overridden by Bidi
*
* @param entityDefinition
* the entity which contains the page part is based on
* @param pagePart
* the page part
* @param width
* the page part columns width
*/
protected void calculateWidth(ScreenEntityDefinition entityDefinition, SimplePagePartDefinition pagePart, int width) {
int widthPercentage = 100 * width / entityDefinition.getScreenSize().getColumns();
pagePart.setWidth(widthPercentage);
}
protected void calculatePartPosition(List<ScreenFieldDefinition> neighbourfields, ScreenEntityDefinition entityDefinition,
SimplePagePartDefinition pagePart, int startColumn) {
ScreenFieldDefinition firstField = neighbourfields.get(0);
TerminalPosition firstFieldPosition = firstField.getPosition();
ScreenSize screenSize = entityDefinition.getScreenSize();
int topMarginPercentage = calculateTopMargin(screenSize, firstFieldPosition.getRow());
int topLeftColumn = startColumn + defaultLeftMarginOffset;
int leftMarginPercentage = calculateLeftMargin(screenSize, topLeftColumn);
pagePart.setTopMargin(topMarginPercentage);
pagePart.setLeftMargin(leftMarginPercentage);
}
private static int calculateTopMargin(ScreenSize screenSize, int row) {
return (100 * (row - 1)) / screenSize.getRows();
}
private int calculateLeftMargin(ScreenSize screenSize, int topLeftColumn) {
return ((100 * topLeftColumn) / screenSize.getColumns()) + defaultTopMarginOffset;
}
/**
* Calculates the most left column of a field, considering the label position
*
* @param field
* @return start column of the field
*/
protected int calculateStartColumn(ScreenFieldDefinition field) {
if (field.getLabelPosition() != null) {
return field.getLabelPosition().getColumn();
} else {
int column = field.getPosition().getColumn() - (field.getDisplayName().length() + labelFieldDistance);
return column > 0 ? column : 1;
}
}
/**
* Calculates the most right column of a field, considering the label position
*
* @param field
* @return start column of the field
*/
protected int calculateEndColumn(ScreenFieldDefinition screenFieldDefinition) {
int fieldEndColumn = screenFieldDefinition.getLength() > 0 ? screenFieldDefinition.getEndPosition().getColumn()
: screenFieldDefinition.getPosition().getColumn() + defaultFieldLength;
return fieldEndColumn + getAdditionalPartWidth();
}
/**
* Group all fields which are close to each other by a given row (1 - default max), or same row with max distance column (10)
*
* @param sortedFields
* @return list of neighbor fields
*/
private List<List<ScreenFieldDefinition>> groupNeighbourFields(List<ScreenFieldDefinition> sortedFields) {
List<List<ScreenFieldDefinition>> neighourFieldsGroups = new ArrayList<List<ScreenFieldDefinition>>();
for (ScreenFieldDefinition screenFieldDefinition : sortedFields) {
boolean found = false;
for (List<ScreenFieldDefinition> neighourFields : neighourFieldsGroups) {
for (ScreenFieldDefinition neighourField : neighourFields) {
if (isBelowNighbour(screenFieldDefinition, neighourField)
|| isRightNeighbour(screenFieldDefinition.getPosition(), neighourField)) {
logger.debug(MessageFormat.format("Adding field definition {0} to neighbour fields: {1}",
screenFieldDefinition, neighourFields));
neighourFields.add(screenFieldDefinition);
found = true;
break;
}
}
}
if (!found) {
List<ScreenFieldDefinition> neighourFields = new ArrayList<ScreenFieldDefinition>();
neighourFields.add(screenFieldDefinition);
neighourFieldsGroups.add(neighourFields);
}
}
return neighourFieldsGroups;
}
private boolean isRightNeighbour(TerminalPosition fieldPosition, ScreenFieldDefinition neighourField) {
TerminalPosition neighbourEndPosition = neighourField.getEndPosition();
return fieldPosition.getColumn() <= neighbourEndPosition.getColumn() + maxColumnDistanceWithinPart
&& fieldPosition.getRow() == neighbourEndPosition.getRow();
}
private boolean isBelowNighbour(ScreenFieldDefinition screenFieldDefinition, ScreenFieldDefinition neighbourField) {
TerminalPosition fieldPosition = screenFieldDefinition.getPosition();
TerminalPosition neighbourFieldPosition = neighbourField.getPosition();
int belowColumnsDistance = Math.abs(fieldPosition.getColumn() - neighbourFieldPosition.getColumn());
int belowRowsDistance = Math.abs(fieldPosition.getRow() - neighbourFieldPosition.getRow());
boolean isBelowNeighbour = belowColumnsDistance <= maxNeighbourColumnsOffset
&& belowRowsDistance <= maxRowDistanceWithinPart;
if (isBelowNeighbour) {
return true;
}
TerminalPosition fieldLabelPosition = screenFieldDefinition.getLabelPosition();
TerminalPosition neighbourFieldLabelPosition = neighbourField.getLabelPosition();
if (fieldLabelPosition == null || neighbourFieldLabelPosition == null) {
return false;
}
boolean underNeighbourByLabel = fieldLabelPosition.getColumn() == neighbourFieldLabelPosition.getColumn()
&& fieldPosition.getRow() - maxRowDistanceWithinPart == neighbourFieldPosition.getRow();
return underNeighbourByLabel;
}
protected int getLabelFieldDistance() {
return labelFieldDistance;
}
public void setLabelFieldDistance(int labelFieldDistance) {
this.labelFieldDistance = labelFieldDistance;
}
public void setMaxRowDistanceWithinPart(int maxRowDistanceWithinPart) {
this.maxRowDistanceWithinPart = maxRowDistanceWithinPart;
}
public void setMaxColumnDistanceWithinPart(int maxColumnDistanceWithinPart) {
this.maxColumnDistanceWithinPart = maxColumnDistanceWithinPart;
}
public void setMaxNeighbourColumnsOffset(int maxNeighbourColumnsOffset) {
this.maxNeighbourColumnsOffset = maxNeighbourColumnsOffset;
}
public void setDefaultFieldLength(int defaultFieldLength) {
this.defaultFieldLength = defaultFieldLength;
}
public void setDefaultLeftMarginOffset(int defaultLeftMarginOffset) {
this.defaultLeftMarginOffset = defaultLeftMarginOffset;
}
protected int getDefaultLeftMarginOffset() {
return defaultLeftMarginOffset;
}
protected int getDefaultFieldLength() {
return defaultFieldLength;
}
protected int getDefaultTopMarginOffset() {
return defaultTopMarginOffset;
}
public void setDefaultTopMarginOffset(int defaultTopMarginOffset) {
this.defaultTopMarginOffset = defaultTopMarginOffset;
}
public int getAdditionalPartWidth() {
return additionalPartWidth;
}
public void setAdditionalPartWidth(int additionalPartWidth) {
this.additionalPartWidth = additionalPartWidth;
}
}