/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2006 - 2015 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.layout;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.util.ArrayList;
import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.internal.matchers.GreaterOrEqual;
import org.mockito.internal.matchers.LessOrEqual;
import org.pentaho.reporting.engine.classic.core.Band;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.Element;
import org.pentaho.reporting.engine.classic.core.ElementAlignment;
import org.pentaho.reporting.engine.classic.core.MasterReport;
import org.pentaho.reporting.engine.classic.core.SimplePageDefinition;
import org.pentaho.reporting.engine.classic.core.elementfactory.LabelElementFactory;
import org.pentaho.reporting.engine.classic.core.layout.model.LogicalPageBox;
import org.pentaho.reporting.engine.classic.core.layout.model.ParagraphRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableText;
import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys;
import org.pentaho.reporting.engine.classic.core.style.TextStyleKeys;
import org.pentaho.reporting.engine.classic.core.testsupport.DebugReportRunner;
import org.pentaho.reporting.engine.classic.core.testsupport.selector.MatchFactory;
import org.pentaho.reporting.engine.classic.core.util.PageFormatFactory;
import org.pentaho.reporting.engine.classic.core.util.PageSize;
import org.pentaho.reporting.libraries.base.util.FloatDimension;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.pentaho.reporting.engine.classic.core.ClassicEngineCoreModule.COMPLEX_TEXT_CONFIG_OVERRIDE_KEY;
/*
* A tip for debugging:
* ModelPrinter.INSTANCE.print() is useful for printing layout to console
*/
public class WordWrapLayoutIT {
private static final String LONG_WORD = "a-long-word-where-parts-are-separated-with-hyphen";
private static final String SHORT_WORD = "word";
private static final float LABEL_MAX_WIDTH_NORMAL = 40;
private static final float LABEL_MAX_WIDTH_TINY = 1;
private static final String PARAGRAPH_NAME = "paragraph-for-picking-up";
@BeforeClass
public static void setUp() throws Exception {
ClassicEngineBoot.getInstance().start();
}
@Test
public void isBreakingWords_WhenWordBreakPropertyIsTrue() throws Exception {
MasterReport report = createReport( true );
ParagraphRenderBox paragraph = pickupParagraph( report );
List<RenderableText> textNodes = pickupTextNodes( paragraph );
assertTextNodesLayInsideBoxBounds( paragraph, textNodes );
}
@Test
public void isNotBreakingWords_WhenWordBreakPropertyIsFalse() throws Exception {
MasterReport report = createReport( false );
ParagraphRenderBox paragraph = pickupParagraph( report );
List<RenderableText> textNodes = pickupTextNodes( paragraph );
assertEquals( "The only word should not be split", 1, textNodes.size() );
}
private static MasterReport createReport( boolean allowWordBreak ) {
return createReport( LONG_WORD, allowWordBreak, LABEL_MAX_WIDTH_NORMAL );
}
@Test
public void isNotBreakingWords_WhenWordFitsIntoBounds() throws Exception {
MasterReport report = createReport( SHORT_WORD );
ParagraphRenderBox paragraph = pickupParagraph( report );
List<RenderableText> textNodes = pickupTextNodes( paragraph );
assertTextNodesLayInsideBoxBounds( paragraph, textNodes );
assertEquals( "The word is too short to be split", 1, textNodes.size() );
}
private static MasterReport createReport( String word ) {
return createReport( word, true, LABEL_MAX_WIDTH_NORMAL );
}
@Test
public void isNotBreakingWords_WhenThereIsNoEnoughSpaceEvenForOneChar() throws Exception {
MasterReport report = createReport( LONG_WORD, true, LABEL_MAX_WIDTH_TINY );
ParagraphRenderBox paragraph = pickupParagraph( report );
List<RenderableText> textNodes = pickupTextNodes( paragraph );
assertEquals( "The split should be not done as label's max width is not enough to contain even one char", 1,
textNodes.size() );
}
@Test
public void isBreakingWords_SimilarlyForAllAlignments_LeftVsRight() throws Exception {
testBreakingWordsIsSimilarForLeftAnd( ElementAlignment.RIGHT );
}
@Test
public void isBreakingWords_SimilarlyForAllAlignments_LeftVsCenter() throws Exception {
testBreakingWordsIsSimilarForLeftAnd( ElementAlignment.CENTER );
}
@Test
public void isBreakingWords_SimilarlyForAllAlignments_LeftVsJustify() throws Exception {
testBreakingWordsIsSimilarForLeftAnd( ElementAlignment.JUSTIFY );
}
/*
* Word breaks are the last action done over a word to cram it into its container's bounds. Hence word-breaking should
* be done over the word similarly regardless its alignment.
*
* Important! The assumption above is true for the ideal case, which means that the text should be a single word.
* Otherwise it is difficult to predict final layout
*/
private void testBreakingWordsIsSimilarForLeftAnd( ElementAlignment alignment ) throws Exception {
ParagraphRenderBox paragraphLeft = pickupParagraph( createReport( ElementAlignment.LEFT ) );
List<RenderableText> textNodesLeft = pickupTextNodes( paragraphLeft );
ParagraphRenderBox paragraphA = pickupParagraph( createReport( alignment ) );
List<RenderableText> textNodesA = pickupTextNodes( paragraphA );
assertTextNodesAreSimilar( textNodesLeft, textNodesA );
}
private static MasterReport createReport( ElementAlignment alignment ) {
return createReport( LONG_WORD, true, LABEL_MAX_WIDTH_NORMAL, alignment );
}
private void assertTextNodesAreSimilar( List<RenderableText> first, List<RenderableText> second ) throws Exception {
// similarity means:
// 1) amounts of chunks are equal
// 2) each pair of chunks refers to the same chars
assertEquals( first.size(), second.size() );
for ( int i = 0; i < first.size(); i++ ) {
assertEquals( first.get( i ).getRawText(), second.get( i ).getRawText() );
}
}
private static MasterReport createReport( String word, boolean allowWordBreak, float labelMaxWidth ) {
return createReport( word, allowWordBreak, labelMaxWidth, ElementAlignment.LEFT );
}
private static MasterReport createReport( String word, boolean allowWordBreak, float labelMaxWidth,
ElementAlignment alignment ) {
MasterReport report = new MasterReport();
// force not to use complex text processing
report.getReportConfiguration().setConfigProperty( COMPLEX_TEXT_CONFIG_OVERRIDE_KEY, "false" );
PageFormatFactory pff = PageFormatFactory.getInstance();
Paper paper = pff.createPaper( PageSize.A4 );
pff.setBorders( paper, 36.0, 36.0, 36.0, 36.0 );
PageFormat format = pff.createPageFormat( paper, PageFormat.PORTRAIT );
report.setPageDefinition( new SimplePageDefinition( format ) );
Band pageHeader = report.getPageHeader();
pageHeader.getStyle().setStyleProperty( ElementStyleKeys.MIN_HEIGHT, 10.0f );
// the main heading is just a fixed label
LabelElementFactory labelFactory = new LabelElementFactory();
labelFactory.setText( word );
labelFactory.setFontName( "SansSerif" );
labelFactory.setFontSize( 10 );
labelFactory.setDynamicHeight( true );
// will be useful when printing the page on a real monitor as it highlights paragraph's area
labelFactory.setBackgroundColor( Color.YELLOW );
labelFactory.setAbsolutePosition( new Point2D.Double( 15, 10 ) );
labelFactory.setMinimumSize( new FloatDimension( 40, 10 ) );
labelFactory.setMaximumWidth( labelMaxWidth );
labelFactory.setHorizontalAlignment( alignment );
Element element = labelFactory.createElement();
element.setName( PARAGRAPH_NAME );
element.getStyle().setStyleProperty( TextStyleKeys.WORDBREAK, allowWordBreak );
pageHeader.addElement( element );
return report;
}
private static ParagraphRenderBox pickupParagraph( MasterReport report ) throws Exception {
LogicalPageBox pageBox = DebugReportRunner.layoutSingleBand( report, report.getPageHeader(), false, false );
RenderNode elementByName = MatchFactory.findElementByName( pageBox, PARAGRAPH_NAME );
assertThat( elementByName, is( instanceOf( ParagraphRenderBox.class ) ) );
return (ParagraphRenderBox) elementByName;
}
private static List<RenderableText> pickupTextNodes( RenderBox box ) {
List<RenderableText> textNodes = new ArrayList<RenderableText>();
RenderNode firstChild = box.getFirstChild();
while ( firstChild != null ) {
if ( firstChild instanceof RenderBox ) {
textNodes.addAll( pickupTextNodes( (RenderBox) firstChild ) );
} else if ( firstChild instanceof RenderableText ) {
textNodes.add( (RenderableText) firstChild );
}
firstChild = firstChild.getNext();
}
return textNodes;
}
private void assertTextNodesLayInsideBoxBounds( RenderBox box, List<RenderableText> texts ) {
assertFalse( texts.isEmpty() );
GreaterOrEqual<Long> greaterThanBoxX = new GreaterOrEqual<Long>( box.getX() );
LessOrEqual<Long> lessThanBoxWidth = new LessOrEqual<Long>( box.getWidth() );
for ( RenderableText text : texts ) {
assertThat( text.getX(), is( greaterThanBoxX ) );
assertThat( text.getWidth(), is( lessThanBoxWidth ) );
}
}
}