From 4e73b876c8e4ae7d56b093c80fa1684d7f38d60b Mon Sep 17 00:00:00 2001 From: Tim Hewson Date: Thu, 4 Apr 2019 13:41:13 +0800 Subject: [PATCH 1/2] Added ability to fill a vertically-filled page with blank data rows. --- .../sf/jasperreports/engine/JRParameter.java | 5 + .../engine/fill/JRBaseFiller.java | 7 + .../engine/fill/JRFillElement.java | 3 + .../engine/fill/JRVerticalFiller.java | 81 + .../net/sf/jasperreports/AbstractTest.java | 22 +- .../bands/fill/BandFillTest.java | 94 + .../bands/fill/repo/BandFillReport.1.jrxml | 87 + .../repo/BandFillReport.1.reference.jrpxml | 1536 +++++++++++++++++ 8 files changed, 1834 insertions(+), 1 deletion(-) create mode 100644 jasperreports/tests/net/sf/jasperreports/bands/fill/BandFillTest.java create mode 100644 jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.jrxml create mode 100644 jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.reference.jrpxml diff --git a/jasperreports/src/net/sf/jasperreports/engine/JRParameter.java b/jasperreports/src/net/sf/jasperreports/engine/JRParameter.java index 9628eb3497..8b6e71256e 100644 --- a/jasperreports/src/net/sf/jasperreports/engine/JRParameter.java +++ b/jasperreports/src/net/sf/jasperreports/engine/JRParameter.java @@ -262,6 +262,11 @@ public interface JRParameter extends JRPropertiesHolder, JRCloneable */ public static final String REPORT_FORMAT_FACTORY = "REPORT_FORMAT_FACTORY"; + /** + * An option allowing the remainder of a vertically-filled report's detail section to be filled with blank rows of + * data when the main data set is exhausted. + */ + public static final String REPORT_FILL_DETAIL_WITH_BLANK_ROWS = "REPORT_FILL_DETAIL_WITH_BLANK_ROWS"; /** * This built-in flag parameter specifies whether to ignore pagination. diff --git a/jasperreports/src/net/sf/jasperreports/engine/fill/JRBaseFiller.java b/jasperreports/src/net/sf/jasperreports/engine/fill/JRBaseFiller.java index b8a4a47845..f7044cd3fd 100644 --- a/jasperreports/src/net/sf/jasperreports/engine/fill/JRBaseFiller.java +++ b/jasperreports/src/net/sf/jasperreports/engine/fill/JRBaseFiller.java @@ -1194,6 +1194,13 @@ protected boolean next() throws JRException return mainDataset.next(); } + /** + * @return whether elements should evaluate as blank + */ + protected boolean evaluateElementToBlank() { + return false; + } + /** * Resolves elements which are to be evaluated at report level. */ diff --git a/jasperreports/src/net/sf/jasperreports/engine/fill/JRFillElement.java b/jasperreports/src/net/sf/jasperreports/engine/fill/JRFillElement.java index 36f6cb6c23..997c925242 100644 --- a/jasperreports/src/net/sf/jasperreports/engine/fill/JRFillElement.java +++ b/jasperreports/src/net/sf/jasperreports/engine/fill/JRFillElement.java @@ -1166,6 +1166,9 @@ protected boolean delayedEvaluationUpdatesTemplate() */ public final Object evaluateExpression(JRExpression expression, byte evaluation) throws JRException { + if (filler.evaluateElementToBlank()) { + return null; + } return expressionEvaluator.evaluate(expression, evaluation); } diff --git a/jasperreports/src/net/sf/jasperreports/engine/fill/JRVerticalFiller.java b/jasperreports/src/net/sf/jasperreports/engine/fill/JRVerticalFiller.java index 75a76fe455..321f88df51 100644 --- a/jasperreports/src/net/sf/jasperreports/engine/fill/JRVerticalFiller.java +++ b/jasperreports/src/net/sf/jasperreports/engine/fill/JRVerticalFiller.java @@ -23,6 +23,8 @@ */ package net.sf.jasperreports.engine.fill; +import java.util.Map; + import org.apache.commons.javaflow.api.continuable; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -30,7 +32,9 @@ import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.JRExpression; import net.sf.jasperreports.engine.JRGroup; +import net.sf.jasperreports.engine.JRParameter; import net.sf.jasperreports.engine.JRRuntimeException; +import net.sf.jasperreports.engine.JasperPrint; import net.sf.jasperreports.engine.JasperReport; import net.sf.jasperreports.engine.JasperReportsContext; import net.sf.jasperreports.engine.type.FooterPositionEnum; @@ -47,6 +51,16 @@ public class JRVerticalFiller extends JRBaseFiller private static final Log log = LogFactory.getLog(JRVerticalFiller.class); + // Whether we are configured to fill the detail section with blank rows of data. + // Is configured to not fill by default. + private boolean fillDetailWithBlankRows = false; + + // Flag to indicate whether we've reached the end of our main data set. + private boolean isEndOfMainDataset = false; + + // Flag to indicate when we're writing the detail section. + private boolean writingDetailSection = false; + /** * */ @@ -81,6 +95,26 @@ public JRVerticalFiller( setPageHeight(pageHeight); } + @Override + protected void setParameters(Map parameterValues) throws JRException + { + super.setParameters(parameterValues); + + setFillDetailWithBlankRows(parameterValues); + } + + protected void setFillDetailWithBlankRows(Map parameterValues) + { + Object value = parameterValues.getOrDefault(JRParameter.REPORT_FILL_DETAIL_WITH_BLANK_ROWS, Boolean.FALSE); + if (value instanceof String) + { + fillDetailWithBlankRows = Boolean.parseBoolean((String)value); + } else if (value instanceof Boolean) + { + fillDetailWithBlankRows = ((Boolean)value).booleanValue(); + } + } + @Override protected void setPageHeight(int pageHeight) @@ -98,6 +132,15 @@ protected void setPageHeight(int pageHeight) } } + /** + * {@inheritDoc} + */ + @Override + public JasperPrint fill(Map parameterValues) throws JRException + { + isEndOfMainDataset = false; + return super.fill(parameterValues); + } @Override @continuable @@ -231,7 +274,42 @@ protected synchronized void fillReport() throws JRException } } + /** + * {@inheritDoc} + */ + @Override + protected boolean next() throws JRException { + if (super.next()) { + return true; + } + if (fillDetailWithBlankRows) { + // If we have no more data in our main data set and we are configured to fill will blank rows, + // move the cursor forward until we have reached the end of the current page's detail section. + isEndOfMainDataset = true; + return !endOfPageDetail(); + } + return false; + } + + private boolean endOfPageDetail() { + JRFillBand[] detailBands = detailSection.getFillBands(); + for (int i = 0; i < detailBands.length; i++) { + JRFillBand detailBand = detailBands[i]; + if (detailBand.isToPrint() && (detailBand.getBreakHeight() > columnFooterOffsetY - offsetY)) { + return true; + } + } + return false; + } + /** + * {@inheritDoc} + */ + @Override + protected boolean evaluateElementToBlank() { + return isEndOfMainDataset && writingDetailSection; + } + /** * */ @@ -752,6 +830,8 @@ private void fillGroupHeaderReprint(JRFillGroup group, byte evaluation) throws J @continuable private void fillDetail() throws JRException { + writingDetailSection = true; + if (log.isDebugEnabled() && !detailSection.isEmpty()) { log.debug("Fill " + fillerId + ": detail at " + offsetY); @@ -851,6 +931,7 @@ private void fillDetail() throws JRException isNewPage = false; isNewColumn = false; + writingDetailSection = false; } diff --git a/jasperreports/tests/net/sf/jasperreports/AbstractTest.java b/jasperreports/tests/net/sf/jasperreports/AbstractTest.java index d1b42aaa37..7fbdf5c51b 100644 --- a/jasperreports/tests/net/sf/jasperreports/AbstractTest.java +++ b/jasperreports/tests/net/sf/jasperreports/AbstractTest.java @@ -38,12 +38,14 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.TimeZone; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.testng.annotations.BeforeClass; +import net.sf.jasperreports.engine.JRDataSource; import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.JRParameter; import net.sf.jasperreports.engine.JasperCompileManager; @@ -103,8 +105,11 @@ protected void runReport(String jrxmlFileName, String referenceFileNamePrefix) HashMap params = new HashMap(); params.put(JRParameter.REPORT_LOCALE, Locale.US); params.put(JRParameter.REPORT_TIME_ZONE, TimeZone.getTimeZone("GMT")); + params.putAll(additionalReportParams()); params.put(TEST, this); + JRDataSource dataSource = createDataSource(); + log.debug("Running report " + jrxmlFileName); try @@ -112,7 +117,14 @@ protected void runReport(String jrxmlFileName, String referenceFileNamePrefix) JasperReport report = compileReport(jrxmlFileName); if (report != null) { - JasperPrint print = fillManager.fill(report, params); + JasperPrint print; + if (dataSource == null) + { + print = fillManager.fill(report, params); + } else + { + print = fillManager.fill(report, params, dataSource); + } assert !print.getPages().isEmpty(); @@ -154,6 +166,14 @@ protected void runReport(String jrxmlFileName, String referenceFileNamePrefix) } } + protected Map additionalReportParams() { + return new HashMap<>(); + } + + protected JRDataSource createDataSource() { + return null; + } + protected JasperReportsContext getJasperReportsContext() { return jasperReportsContext; diff --git a/jasperreports/tests/net/sf/jasperreports/bands/fill/BandFillTest.java b/jasperreports/tests/net/sf/jasperreports/bands/fill/BandFillTest.java new file mode 100644 index 0000000000..5f92c1a737 --- /dev/null +++ b/jasperreports/tests/net/sf/jasperreports/bands/fill/BandFillTest.java @@ -0,0 +1,94 @@ +/* + * JasperReports - Free Java Reporting Library. + * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved. + * http://www.jaspersoft.com + * + * Unless you have purchased a commercial license agreement from Jaspersoft, + * the following license terms apply: + * + * This program is part of JasperReports. + * + * JasperReports is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * JasperReports 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with JasperReports. If not, see . + */ +package net.sf.jasperreports.bands.fill; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.IntStream; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import net.sf.jasperreports.AbstractTest; +import net.sf.jasperreports.engine.JRDataSource; +import net.sf.jasperreports.engine.JRException; +import net.sf.jasperreports.engine.JRParameter; +import net.sf.jasperreports.engine.data.JRBeanArrayDataSource; + +/** @author Tim Hewson */ +public class BandFillTest extends AbstractTest +{ + + @Override + protected Map additionalReportParams() + { + Map params = new HashMap(); + params.put(JRParameter.REPORT_FILL_DETAIL_WITH_BLANK_ROWS, Boolean.TRUE); + return params; + } + + + @Override + protected JRDataSource createDataSource() + { + return new JRBeanArrayDataSource( + IntStream.range(0, 90) + .mapToObj(i -> new Ascii(33 + i, String.valueOf((char) (33 + i)))) + .toArray()); + } + + @Test(dataProvider = "testArgs") + public void testReport(String jrxmlFileName, String referenceFileNamePrefix) + throws JRException, NoSuchAlgorithmException, IOException + { + runReport(jrxmlFileName, referenceFileNamePrefix); + } + + @DataProvider + public Object[][] testArgs() + { + return runReportArgs("net/sf/jasperreports/bands/fill/repo", "BandFillReport", 40); + } + + public static class Ascii + { + long record_num; + String record_val; + + Ascii(long number, String value) { + this.record_num = number; + this.record_val = value; + } + + public long getRecord_num() { + return record_num; + } + + public String getRecord_val() { + return record_val; + } + } +} diff --git a/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.jrxml b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.jrxml new file mode 100644 index 0000000000..eced6174cb --- /dev/null +++ b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.jrxml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + <band height="22" splitType="Stretch"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.reference.jrpxml b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.reference.jrpxml new file mode 100644 index 0000000000..e30dfdac67 --- /dev/null +++ b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandFillReport.1.reference.jrpxml @@ -0,0 +1,1536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 6bc9379d8a978ab1515bf5140fd810d195460304 Mon Sep 17 00:00:00 2001 From: Tim Hewson Date: Tue, 16 Apr 2019 15:34:35 +0800 Subject: [PATCH 2/2] Added test to show fill with stretched content. --- .../bands/fill/BandWithStretchFillTest.java | 110 + .../repo/BandWithStretchFillReport.1.jrxml | 84 + ...ndWithStretchFillReport.1.reference.jrpxml | 1772 +++++++++++++++++ 3 files changed, 1966 insertions(+) create mode 100644 jasperreports/tests/net/sf/jasperreports/bands/fill/BandWithStretchFillTest.java create mode 100644 jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.jrxml create mode 100644 jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.reference.jrpxml diff --git a/jasperreports/tests/net/sf/jasperreports/bands/fill/BandWithStretchFillTest.java b/jasperreports/tests/net/sf/jasperreports/bands/fill/BandWithStretchFillTest.java new file mode 100644 index 0000000000..9e8cca14b3 --- /dev/null +++ b/jasperreports/tests/net/sf/jasperreports/bands/fill/BandWithStretchFillTest.java @@ -0,0 +1,110 @@ +/* + * JasperReports - Free Java Reporting Library. + * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved. + * http://www.jaspersoft.com + * + * Unless you have purchased a commercial license agreement from Jaspersoft, + * the following license terms apply: + * + * This program is part of JasperReports. + * + * JasperReports is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * JasperReports 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with JasperReports. If not, see . + */ +package net.sf.jasperreports.bands.fill; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.IntStream; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import net.sf.jasperreports.AbstractTest; +import net.sf.jasperreports.engine.JRDataSource; +import net.sf.jasperreports.engine.JRException; +import net.sf.jasperreports.engine.JRParameter; +import net.sf.jasperreports.engine.data.JRBeanArrayDataSource; + +/** @author Tim Hewson */ +public class BandWithStretchFillTest extends AbstractTest +{ + private String data[] = new String[] { + "Word.", + "A series of words.", + "This is a short sentence.", + "This is a longer sentence that will cause the cell to stretch across two lines.", + "This is an even longer sentence that will cause the cell to stretch across three lines in total."}; + + @Override + protected Map additionalReportParams() + { + Map params = new HashMap(); + params.put(JRParameter.REPORT_FILL_DETAIL_WITH_BLANK_ROWS, Boolean.TRUE); + return params; + } + + + @Override + protected JRDataSource createDataSource() + { + return new JRBeanArrayDataSource( + IntStream.range(1, 91) + .mapToObj(i -> { + String text = null; + int d = data.length; + while (text == null) { + if (i % d == 0) { + text = data[d - 1]; + } + d--; + } + return new Data(i, text); + }) + .toArray()); + } + + @Test(dataProvider = "testArgs") + public void testReport(String jrxmlFileName, String referenceFileNamePrefix) + throws JRException, NoSuchAlgorithmException, IOException + { + runReport(jrxmlFileName, referenceFileNamePrefix); + } + + @DataProvider + public Object[][] testArgs() + { + return runReportArgs("net/sf/jasperreports/bands/fill/repo", "BandWithStretchFillReport", 40); + } + + public static class Data + { + long record_num; + String record_val; + + Data(long number, String value) { + this.record_num = number; + this.record_val = value; + } + + public long getRecord_num() { + return record_num; + } + + public String getRecord_val() { + return record_val; + } + } +} diff --git a/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.jrxml b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.jrxml new file mode 100644 index 0000000000..7ce32de8c1 --- /dev/null +++ b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.jrxml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.reference.jrpxml b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.reference.jrpxml new file mode 100644 index 0000000000..a8eb234942 --- /dev/null +++ b/jasperreports/tests/net/sf/jasperreports/bands/fill/repo/BandWithStretchFillReport.1.reference.jrpxml @@ -0,0 +1,1772 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +