Coverage Report - net.objectlab.qalab.m2.report.MergeAndChartReport
 
Classes in this File Line Coverage Branch Coverage Complexity
MergeAndChartReport
0% 
0% 
2
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 //
 3  
 //                  ObjectLab is sponsoring QALab
 4  
 //
 5  
 // Based in London, we are world leaders in the design and development
 6  
 // of bespoke applications for the Securities Financing markets.
 7  
 //
 8  
 // <a href="http://www.objectlab.co.uk/open">Click here to learn more</a>
 9  
 //           ___  _     _           _   _          _
 10  
 //          / _ \| |__ (_) ___  ___| |_| |    __ _| |__
 11  
 //         | | | | '_ \| |/ _ \/ __| __| |   / _` | '_ \
 12  
 //         | |_| | |_) | |  __/ (__| |_| |__| (_| | |_) |
 13  
 //          \___/|_.__// |\___|\___|\__|_____\__,_|_.__/
 14  
 //                   |__/
 15  
 //
 16  
 //                   http://www.ObjectLab.co.uk
 17  
 // ---------------------------------------------------------------------------
 18  
 //
 19  
 //QALab is released under the GNU General Public License.
 20  
 //
 21  
 //QALab: Collects QA Statistics from your build over time.
 22  
 //2005+, ObjectLab Ltd
 23  
 //
 24  
 //This library is free software; you can redistribute it and/or
 25  
 //modify it under the terms of the GNU General Public
 26  
 //License as published by the Free Software Foundation; either
 27  
 //version 2.1 of the License, or (at your option) any later version.
 28  
 //
 29  
 //This library is distributed in the hope that it will be useful,
 30  
 //but WITHOUT ANY WARRANTY; without even the implied warranty of
 31  
 //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 32  
 //General Public License for more details.
 33  
 //
 34  
 //You should have received a copy of the GNU General Public
 35  
 //License along with this library; if not, write to the Free Software
 36  
 //Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 37  
 //
 38  
 ////////////////////////////////////////////////////////////////////////////////
 39  
 package net.objectlab.qalab.m2.report;
 40  
 
 41  
 import java.io.File;
 42  
 import java.io.IOException;
 43  
 import java.util.Locale;
 44  
 import java.util.Properties;
 45  
 import java.util.ResourceBundle;
 46  
 import java.io.FileInputStream;
 47  
 import java.io.InputStream;
 48  
 
 49  
 import net.objectlab.qalab.m2.BuildStatChartMojo;
 50  
 import net.objectlab.qalab.m2.QALabStatAllMergeMojo;
 51  
 import net.objectlab.qalab.m2.util.Utils;
 52  
 import net.objectlab.qalab.m2.util.XmlTransformer;
 53  
 
 54  
 import org.apache.maven.plugin.MojoExecutionException;
 55  
 import org.apache.maven.project.MavenProject;
 56  
 import org.apache.maven.reporting.AbstractMavenReport;
 57  
 import org.apache.maven.reporting.MavenReportException;
 58  
 
 59  
 import org.codehaus.doxia.site.renderer.SiteRenderer;
 60  
 import javax.xml.transform.TransformerException;
 61  
 
 62  
 /**
 63  
  * *** IMPORTANT USE this Report as part of your reporting section, it merges the data in qalab.xml,
 64  
  * creates the charts and generates the chart report, ensure that this report is run AFTER Checkstyle,
 65  
  * pmd, Findbugs, Cobertura, etc.
 66  
  *
 67  
  * @author Benoit Xhenseval.
 68  
  * @goal report-merge-chart
 69  
  * @phase deploy
 70  
  */
 71  0
 public class MergeAndChartReport extends AbstractMavenReport {
 72  
 
 73  
     // ~ MERGE fields -------------------------------------------------------
 74  
     /**
 75  
      * The input file generated by Checkstyle.
 76  
      *
 77  
      * @parameter expression="${project.build.directory}/checkstyle-result.xml"
 78  
      */
 79  0
     private File checkstyleInputFile = null;
 80  
 
 81  
     /**
 82  
      * The fully qualified class name for the handler for Checkstyle.
 83  
      *
 84  
      * @parameter default-value="net.objectlab.qalab.parser.CheckstyleStatMerge"
 85  
      */
 86  
     private String checkstyleHandler;
 87  
 
 88  
     /**
 89  
      * The input file generated by PMD.
 90  
      *
 91  
      * @parameter expression="${project.build.directory}/pmd.xml"
 92  
      */
 93  0
     private File pmdInputFile = null;
 94  
 
 95  
     /**
 96  
      * The fully qualified class name for the handler for PMD.
 97  
      *
 98  
      * @parameter default-value="net.objectlab.qalab.parser.PMDStatMerge"
 99  
      */
 100  
     private String pmdHandler;
 101  
 
 102  
     /**
 103  
      * The input file generated by PMD CPD.
 104  
      *
 105  
      * @parameter expression="${project.build.directory}/cpd.xml"
 106  
      */
 107  0
     private File pmdCpdInputFile = null;
 108  
 
 109  
     /**
 110  
      * The fully qualified class name for the handler for PMD CPD.
 111  
      *
 112  
      * @parameter default-value="net.objectlab.qalab.parser.PMDCPDStatMerge"
 113  
      */
 114  
     private String pmdCpdHandler;
 115  
 
 116  
     /**
 117  
      * The input file generated by FindBugs.
 118  
      *
 119  
      * @parameter expression="${project.build.directory}/findbugs.xml"
 120  
      */
 121  0
     private File findbugsInputFile = null;
 122  
 
 123  
     /**
 124  
      * The fully qualified class name for the handler for FindBugs.
 125  
      *
 126  
      * @parameter default-value="net.objectlab.qalab.parser.FindBugsStatMerge"
 127  
      */
 128  
     private String findbugsHandler;
 129  
 
 130  
     /**
 131  
      * The input file generated by Simian.
 132  
      *
 133  
      * @parameter expression="${project.build.directory}/simian-raw-report.xml"
 134  
      */
 135  0
     private File simianInputFile = null;
 136  
 
 137  
     /**
 138  
      * The fully qualified class name for the handler for Simian.
 139  
      *
 140  
      * @parameter default-value="net.objectlab.qalab.parser.SimianStatMerge"
 141  
      */
 142  
     private String simianHandler;
 143  
 
 144  
     /**
 145  
      * The input file generated by Cobertura.
 146  
      *
 147  
      * @parameter expression="${project.reporting.outputDirectory}/cobertura/coverage.xml"
 148  
      */
 149  0
     private File coberturaInputFile = null;
 150  
 
 151  
     /**
 152  
      * The fully qualified class name for the handler for Cobertura Line.
 153  
      *
 154  
      * @parameter default-value="net.objectlab.qalab.parser.CoberturaLineStatMerge"
 155  
      */
 156  
     private String coberturaLineHandler;
 157  
 
 158  
     /**
 159  
      * The fully qualified class name for the handler for Cobertura Branch.
 160  
      *
 161  
      * @parameter default-value="net.objectlab.qalab.parser.CoberturaBranchStatMerge"
 162  
      */
 163  
     private String coberturaBranchHandler;
 164  
 
 165  
     /**
 166  
      * If true then any debug logging output will be suppressed.
 167  
      *
 168  
      * @parameter default-value=false
 169  
      */
 170  0
     private boolean quiet = false;
 171  
 
 172  
     /**
 173  
      * If true then use ONLY DATE for timestamp (use in conjunction action
 174  
      * replace).
 175  
      *
 176  
      * @parameter default-value=true
 177  
      */
 178  0
     private boolean dateOnly = true;
 179  
 
 180  
     /**
 181  
      * The directory where the source code is.
 182  
      *
 183  
      * @parameter expression="${project.build.sourceDirectory}"
 184  
      */
 185  
     private String srcDir;
 186  
 
 187  
     /**
 188  
      * The timestamp for the stats.
 189  
      *
 190  
      * @parameter
 191  
      */
 192  
     private String mergerTimeStamp;
 193  
 
 194  
     /**
 195  
      * An exporter class name.
 196  
      *
 197  
      * @parameter default-value="net.objectlab.qalab.exporter.QALabXMLExporter"
 198  
      */
 199  0
     private String exporterClassName = "net.objectlab.qalab.exporter.QALabXMLExporter";
 200  
 
 201  
     /**
 202  
      * The properties file to use instead of setting them one by one.
 203  
      *
 204  
      * @parameter
 205  
      */
 206  
     private File propertiesFile;
 207  
 
 208  
     /**
 209  
      * the loaded properties from the properties file declared above.
 210  
      */
 211  0
     private Properties theProperties = null;
 212  
 
 213  
     // -------------------- CHART GENERATION ------------------------------------------
 214  
 
 215  
     /**
 216  
      * The directory where the generated html report will go.
 217  
      *
 218  
      * @parameter expression="${project.reporting.outputDirectory}/qalab"
 219  
      * @required
 220  
      */
 221  
     private File chartOutputDirectory;
 222  
 
 223  
     /**
 224  
      * The Chart chartWidth.
 225  
      *
 226  
      * @parameter default-value=750
 227  
      */
 228  
     private int chartWidth;
 229  
 
 230  
     /**
 231  
      * The Chart chartHeight.
 232  
      *
 233  
      * @parameter default-value=500
 234  
      */
 235  
     private int chartHeight;
 236  
 
 237  
     /**
 238  
      * If the movingAverage is &lt;= 0 then there is no moving average, otherwise
 239  
      * it shows the average based on the last n points, where n is the value of
 240  
      * this field.
 241  
      *
 242  
      * @parameter default-value=0
 243  
      */
 244  0
     private int movingAverage = 0;
 245  
 
 246  
     /**
 247  
      * If true then generate a summary chart only.
 248  
      *
 249  
      * @parameter default-value=false
 250  
      */
 251  0
     private boolean chartSummaryOnly = false;
 252  
 
 253  
     /**
 254  
      * Statistic type to appear on summary chart, defaulted to
 255  
      * 'checkstyle,pmd,findbugs,simian,pmd-cpd'.
 256  
      *
 257  
      * @parameter default-value="checkstyle,pmd,findbugs,simian,pmd-cpd"
 258  
      */
 259  0
     private String summaryTypes = "checkstyle,pmd,findbugs,simian,pmd-cpd";
 260  
 
 261  
     /**
 262  
      * File prefix for the charts (e.g. cobertura-) Default empty.
 263  
      *
 264  
      * @parameter default-value=""
 265  
      */
 266  0
     private String chartFilePrefix = "";
 267  
 
 268  
     /**
 269  
      * X Axis Title
 270  
      *
 271  
      * @parameter default-value="Date"
 272  
      */
 273  0
     private String xAxisTitle = "Date";
 274  
 
 275  
     /**
 276  
      * Y Axis Title
 277  
      *
 278  
      * @parameter default-value="Violation Count / Coverage Percent"
 279  
      */
 280  0
     private String yAxisTitle = "Violation Count / Coverage Percent";
 281  
 
 282  
     /**
 283  
      * X Axis Title for Summary
 284  
      *
 285  
      * @parameter default-value="Date"
 286  
      */
 287  0
     private String xAxisSummaryTitle = "Date";
 288  
 
 289  
     /**
 290  
      * Y Axis Title for Summary
 291  
      *
 292  
      * @parameter default-value="Violation Count"
 293  
      */
 294  0
     private String yAxisSummaryTitle = "Violation Count";
 295  
 
 296  
     // -------------------- CHART REPORT ------------------------------------------
 297  
 
 298  
     /**
 299  
      * The generated qalab.xml file.
 300  
      *
 301  
      * @parameter expression="${project.basedir}/qalab.xml"
 302  
      */
 303  0
     private File qalabFile = null;
 304  
 
 305  
     /**
 306  
      * The xslt stylesheet bundled, either qalab-chart-xdoc.xsl or
 307  
      * qalab-chart-html.xsl.
 308  
      *
 309  
      * @parameter default-value="qalab-chart-xdoc.xsl";
 310  
      */
 311  0
     private String chartBundledXsl = null;
 312  
 
 313  
     /**
 314  
      * The directory where the generated xdoc/html report will go.
 315  
      *
 316  
      * @parameter expression="${project.build.directory}/generated-site/xdoc/qalab/"
 317  
      * @required
 318  
      */
 319  
     private String outputDirectory;
 320  
 
 321  
     /**
 322  
      * Statistic types, defaulted to
 323  
      * 'checkstyle,pmd,pmd-cpd,findbugs,simian,cobertura-line,cobertura-branch'.
 324  
      *
 325  
      * @parameter default-value="checkstyle,pmd,pmd-cpd,findbugs,simian,cobertura-line,cobertura-branch"
 326  
      */
 327  0
     private String types = "checkstyle,pmd,pmd-cpd,findbugs,simian,cobertura-line,cobertura-branch";
 328  
 
 329  
     /**
 330  
      * The xml input stream.
 331  
      */
 332  0
     private InputStream theXmlStream = null;
 333  
 
 334  
     /**
 335  
      * The xslt style sheet.
 336  
      *
 337  
      * @parameter
 338  
      */
 339  0
     private File styleSheet = null;
 340  
 
 341  
     /**
 342  
      * The xslt style sheet input stream.
 343  
      */
 344  0
     private InputStream theStyleSheetStream = null;
 345  
 
 346  
     /**
 347  
      * The number of hours to define the time window from now.
 348  
      *
 349  
      * @parameter default-value="48"
 350  
      */
 351  0
     private String startTimeHoursOffset = "48";
 352  
 
 353  
     /**
 354  
      * The string version of the actual date computed by the offset set above.
 355  
      */
 356  
     private String theOffset;
 357  
 
 358  
     /**
 359  
      * Not sure what this is.
 360  
      *
 361  
      * @component
 362  
      */
 363  
     private SiteRenderer siteRenderer;
 364  
 
 365  
     /**
 366  
      * The maven project.
 367  
      *
 368  
      * @parameter expression="${project}"
 369  
      * @required
 370  
      * @readonly
 371  
      */
 372  
     private MavenProject project;
 373  
 
 374  
     /**
 375  
      * generate the actual report.
 376  
      *
 377  
      * @param aLocale
 378  
      *            ignored.
 379  
      * @throws MavenReportException
 380  
      *             if anything goes wrong.
 381  
      */
 382  
     protected final void executeReport(final Locale aLocale) throws MavenReportException {
 383  
 
 384  0
         getLog().info("QALab: Merge and Chart Report");
 385  
 
 386  
         try {
 387  0
             doMerge();
 388  0
         } catch (MojoExecutionException e) {
 389  0
             throw new MavenReportException("Issue during Merge", e);
 390  0
         }
 391  
 
 392  
         try {
 393  0
             doChart();
 394  0
         } catch (MojoExecutionException e) {
 395  0
             throw new MavenReportException("Issue during Chart", e);
 396  0
         }
 397  
 
 398  0
         validate();
 399  
 
 400  0
         getLog().info("Output Directory: " + getOutputDirectory());
 401  
 
 402  0
         final File outdir = new File(getOutputDirectory());
 403  0
         if (!outdir.exists()) {
 404  0
             outdir.mkdirs();
 405  
         }
 406  0
         assert outdir.isDirectory() : "the output directory was not a directory";
 407  
 
 408  
         final File output;
 409  
         try {
 410  0
             output = File.createTempFile("qalab", null, outdir);
 411  0
             assert output != null : "the temp file is null.";
 412  
 
 413  0
         } catch (IOException ioex) {
 414  0
             throw new MavenReportException("could not create temp file.", ioex);
 415  0
         }
 416  
 
 417  0
         final XmlTransformer t = new XmlTransformer(theXmlStream, theStyleSheetStream, output);
 418  0
         t.addParameter("targetdir", getOutputDirectory());
 419  0
         t.addParameter("type", types);
 420  0
         t.addParameter("offset", theOffset);
 421  
         try {
 422  0
             t.transform();
 423  0
         } catch (TransformerException tex) {
 424  0
             throw new MavenReportException("transformation failed.", tex);
 425  0
         }
 426  
 
 427  
         // clean up the temp file when the plugin is done.
 428  0
         output.deleteOnExit();
 429  0
     }
 430  
 
 431  
     private void doMerge() throws MojoExecutionException {
 432  0
         QALabStatAllMergeMojo mojo = new QALabStatAllMergeMojo();
 433  0
         mojo.setCheckstyleHandler(checkstyleHandler);
 434  0
         mojo.setCheckstyleInputFile(checkstyleInputFile);
 435  0
         mojo.setPmdCpdHandler(pmdCpdHandler);
 436  0
         mojo.setPmdCpdInputFile(pmdCpdInputFile);
 437  0
         mojo.setPmdHandler(pmdHandler);
 438  0
         mojo.setPmdInputFile(pmdInputFile);
 439  0
         mojo.setSimianHandler(simianHandler);
 440  0
         mojo.setSimianInputFile(simianInputFile);
 441  0
         mojo.setCoberturaBranchHandler(coberturaBranchHandler);
 442  0
         mojo.setCoberturaInputFile(coberturaInputFile);
 443  0
         mojo.setCoberturaLineHandler(coberturaLineHandler);
 444  0
         mojo.setDateOnly(dateOnly);
 445  0
         mojo.setExporterClassName(exporterClassName);
 446  0
         mojo.setFindbugsHandler(findbugsHandler);
 447  0
         mojo.setFindbugsInputFile(findbugsInputFile);
 448  0
         mojo.setMergerTimeStamp(mergerTimeStamp);
 449  0
         mojo.setOutputFile(qalabFile);
 450  0
         mojo.setPropertiesFile(propertiesFile);
 451  0
         mojo.setQuiet(quiet);
 452  0
         mojo.setSrcDir(srcDir);
 453  0
         mojo.setTheProperties(theProperties);
 454  0
         mojo.setPluginContext(getPluginContext());
 455  
 
 456  0
         mojo.execute();
 457  0
     }
 458  
 
 459  
     private void doChart() throws MojoExecutionException {
 460  0
         BuildStatChartMojo mojo = new BuildStatChartMojo();
 461  0
         mojo.setFilePrefix(chartFilePrefix);
 462  0
         mojo.setSummaryOnly(chartSummaryOnly);
 463  0
         mojo.setSummaryTypes(summaryTypes);
 464  0
         mojo.setHeight(chartHeight);
 465  0
         mojo.setWidth(chartWidth);
 466  0
         mojo.setMovingAverage(movingAverage);
 467  0
         mojo.setQalabFile(qalabFile);
 468  0
         mojo.setQuiet(quiet);
 469  0
         mojo.setToDir(chartOutputDirectory);
 470  0
         mojo.setTypes(types);
 471  0
         mojo.setPluginContext(getPluginContext());
 472  0
         mojo.setXAxisSummaryTitle(xAxisSummaryTitle);
 473  0
         mojo.setXAxisTitle(xAxisTitle);
 474  0
         mojo.setYAxisSummaryTitle(yAxisSummaryTitle);
 475  0
         mojo.setYAxisTitle(yAxisTitle);
 476  
 
 477  0
         mojo.execute();
 478  
 
 479  
         // now just the cobertura report.
 480  0
         mojo.setFilePrefix("cobertura-" + chartFilePrefix);
 481  0
         mojo.setSummaryOnly(true);
 482  0
         mojo.setSummaryTypes("cobertura-line,cobertura-branch");
 483  0
         mojo.setYAxisSummaryTitle("Coverage Percent");
 484  0
         mojo.execute();
 485  0
     }
 486  
 
 487  
     /**
 488  
      * @return "qalab/index"
 489  
      * @see org.apache.maven.reporting.MavenReport#getOutputName()
 490  
      */
 491  
     public final String getOutputName() {
 492  0
         return "qalab/index";
 493  
     }
 494  
 
 495  
     /**
 496  
      * @return The output directory as configured in your <code>pom.xml</code>.
 497  
      * @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory()
 498  
      */
 499  
     protected final String getOutputDirectory() {
 500  0
         return outputDirectory;
 501  
     }
 502  
 
 503  
     /**
 504  
      * @return The Maven Project itself. Used internally to get access to Config
 505  
      *         params etc.
 506  
      * @see org.apache.maven.reporting.AbstractMavenReport#getProject()
 507  
      */
 508  
     protected final MavenProject getProject() {
 509  0
         return project;
 510  
     }
 511  
 
 512  
     /**
 513  
      * @return a direct reference to the site renderer.
 514  
      * @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer()
 515  
      */
 516  
     protected final SiteRenderer getSiteRenderer() {
 517  0
         return siteRenderer;
 518  
     }
 519  
 
 520  
     /**
 521  
      * @param aLocale
 522  
      *            The locale.
 523  
      * @return The locale specific report name.
 524  
      * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale)
 525  
      */
 526  
     public final String getName(final Locale aLocale) {
 527  0
         return getBundle(aLocale).getString("report.qalab.name");
 528  
     }
 529  
 
 530  
     /**
 531  
      * @param aLocale
 532  
      *            The locale.
 533  
      * @return The locale specific report description.
 534  
      * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale)
 535  
      */
 536  
     public final String getDescription(final Locale aLocale) {
 537  0
         return getBundle(aLocale).getString("report.qalab.description");
 538  
     }
 539  
 
 540  
     /**
 541  
      * @return true.
 542  
      * @see org.apache.maven.reporting.MavenReport#isExternalReport
 543  
      */
 544  
     public final boolean isExternalReport() {
 545  0
         return true;
 546  
     }
 547  
 
 548  
     /**
 549  
      * @param aLocale
 550  
      *            The locale.
 551  
      * @return the resource bundle for this report.
 552  
      */
 553  
     private static ResourceBundle getBundle(final Locale aLocale) {
 554  0
         return ResourceBundle.getBundle("qalab-report", aLocale, MergeAndChartReport.class.getClassLoader());
 555  
     }
 556  
 
 557  
     public void setXAxisSummaryTitle(String xAxisSummaryTitle) {
 558  0
             this.xAxisSummaryTitle = xAxisSummaryTitle;
 559  0
     }
 560  
 
 561  
     public void setXAxisTitle(String xAxisTitle) {
 562  0
             this.xAxisTitle = xAxisTitle;
 563  0
     }
 564  
 
 565  
     public void setYAxisSummaryTitle(String yAxisSummaryTitle) {
 566  0
             this.yAxisSummaryTitle = yAxisSummaryTitle;
 567  0
     }
 568  
 
 569  
     public void setYAxisTitle(String yAxisTitle) {
 570  0
             this.yAxisTitle = yAxisTitle;
 571  0
     }
 572  
 
 573  
     /**
 574  
      * Validates the parameters supplied by maven 2.
 575  
      *
 576  
      * @throws MavenReportException
 577  
      *             if any supplied params are wrong.
 578  
      */
 579  
     private void validate() throws MavenReportException {
 580  
         try {
 581  0
             Utils.checkFile(qalabFile, "qalabFile");
 582  0
             theXmlStream = new FileInputStream(qalabFile);
 583  0
         } catch (IOException ioex) {
 584  0
             throw new MavenReportException(ioex.getMessage(), ioex);
 585  0
         }
 586  
 
 587  0
         if (styleSheet == null) {
 588  
             try {
 589  0
                 theStyleSheetStream = Utils.extractAsInputStream(chartBundledXsl);
 590  0
             } catch (IOException ioex) {
 591  0
                 throw new MavenReportException("Could not find the default stylesheet. " + ioex.getMessage(), ioex);
 592  0
             }
 593  
         } else {
 594  
             try {
 595  0
                 Utils.checkFile(styleSheet, "styleSheet");
 596  0
                 theStyleSheetStream = new FileInputStream(styleSheet);
 597  0
             } catch (IOException ioex) {
 598  0
                 throw new MavenReportException(ioex.getMessage(), ioex);
 599  0
             }
 600  
         }
 601  
 
 602  0
         theOffset = Utils.formatDateBasedOnOffset(startTimeHoursOffset);
 603  0
     }
 604  
 }
 605  
 /*
 606  
  *                   ObjectLab is sponsoring QALab
 607  
  *
 608  
  * Based in London, we are world leaders in the design and development
 609  
  * of bespoke applications for the securities financing markets.
 610  
  *
 611  
  * <a href="http://www.objectlab.co.uk/open">Click here to learn more about us</a>
 612  
  *           ___  _     _           _   _          _
 613  
  *          / _ \| |__ (_) ___  ___| |_| |    __ _| |__
 614  
  *         | | | | '_ \| |/ _ \/ __| __| |   / _` | '_ \
 615  
  *         | |_| | |_) | |  __/ (__| |_| |__| (_| | |_) |
 616  
  *          \___/|_.__// |\___|\___|\__|_____\__,_|_.__/
 617  
  *                   |__/
 618  
  *
 619  
  *                     www.ObjectLab.co.uk
 620  
  */