fll.web.report.FinalComputedScores.java Source code

Java tutorial

Introduction

Here is the source code for fll.web.report.FinalComputedScores.java

Source

/*
 * Copyright (c) 2000-2002 INSciTE.  All rights reserved
 * INSciTE is on the web at: http://www.hightechkids.org
 * This code is released under GPL; see LICENSE.txt for details.
 */
package fll.web.report;

import java.io.IOException;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.sql.DataSource;

import org.apache.log4j.Logger;

import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Font;
import com.itextpdf.text.FontFactory;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfPageEventHelper;
import com.itextpdf.text.pdf.PdfTemplate;
import com.itextpdf.text.pdf.PdfWriter;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import fll.Tournament;
import fll.Utilities;
import fll.db.Queries;
import fll.db.TournamentParameters;
import fll.scheduler.TournamentSchedule;
import fll.util.FLLRuntimeException;
import fll.util.FP;
import fll.util.LogUtils;
import fll.util.PdfUtils;
import fll.web.ApplicationAttributes;
import fll.web.BaseFLLServlet;
import fll.xml.ChallengeDescription;
import fll.xml.PerformanceScoreCategory;
import fll.xml.ScoreCategory;
import fll.xml.WinnerType;
import net.mtu.eggplant.util.sql.SQLFunctions;

/**
 * Final computed scores report.
 */
@WebServlet("/report/FinalComputedScores")
public final class FinalComputedScores extends BaseFLLServlet {

    private static final Logger LOGGER = LogUtils.getLogger();

    @Override
    protected void processRequest(final HttpServletRequest request, final HttpServletResponse response,
            final ServletContext application, final HttpSession session) throws IOException, ServletException {

        if (PromptSummarizeScores.checkIfSummaryUpdated(response, application, session,
                "/report/FinalComputedScores")) {
            return;
        }

        Connection connection = null;
        try {
            final DataSource datasource = ApplicationAttributes.getDataSource(application);
            connection = datasource.getConnection();

            final ChallengeDescription challengeDescription = ApplicationAttributes
                    .getChallengeDescription(application);
            final int tournamentID = Queries.getCurrentTournament(connection);
            final Tournament tournament = Tournament.findTournamentByID(connection, tournamentID);

            final int percentageHurdle = TournamentParameters.getPerformanceAdvancementPercentage(connection,
                    tournamentID);
            final double performanceHurdle;
            if (percentageHurdle > 0 && percentageHurdle < 100) {
                // set to a realistic value
                performanceHurdle = percentageHurdle / 100.0;
            } else {
                performanceHurdle = 0;
            }

            final Set<Integer> bestTeams = determineTeamsMeetingPerformanceHurdle(performanceHurdle, connection,
                    tournamentID, challengeDescription.getWinner());

            response.reset();
            response.setContentType("application/pdf");
            response.setHeader("Content-Disposition", "filename=finalComputedScores.pdf");

            final String challengeTitle = challengeDescription.getTitle();
            final FooterHandler pageHandler = new FooterHandler(percentageHurdle);

            generateReport(connection, response.getOutputStream(), challengeDescription, challengeTitle, tournament,
                    pageHandler, bestTeams);

        } catch (final SQLException e) {
            throw new RuntimeException(e);
        } finally {
            SQLFunctions.close(connection);
        }
    }

    /**
     * Determine which teams meet the performance hurdle.
     * This is computed per division and stored in mTeamsMeetingPerformanceHurdle.
     * 
     * @param performanceHurdle the percentage hurdle as a floating point number
     *          between 0 and 1. Outside this range causes the return value to be
     *          empty.
     * @return the set of teams that have a good enough performance score
     * @throws SQLException
     */
    @SuppressFBWarnings(value = {
            "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" }, justification = "Winner criteria determines the sort")
    private Set<Integer> determineTeamsMeetingPerformanceHurdle(final double performanceHurdle,
            final Connection connection, final int tournament, final WinnerType winnerCriteria)
            throws SQLException {

        final Set<Integer> bestTeams = new HashSet<>();
        if (performanceHurdle <= 0 || performanceHurdle >= 1) {
            return bestTeams;
        }

        PreparedStatement prep = null;
        ResultSet rs = null;
        try {
            prep = connection.prepareStatement("SELECT TeamNumber FROM performance_seeding_max, TournamentTeams" //
                    + " WHERE performance_seeding_max.TeamNumber = TournamentTeams.TeamNumber" //
                    + "  AND TournamentTeams.Tournament = ?" //
                    + "  AND TournamentTeams.event_division = ?" //
                    + " ORDER by performance_seeding_max.score " + winnerCriteria.getSortString());
            prep.setInt(1, tournament);

            for (final String division : Queries.getAwardGroups(connection)) {
                final Set<Integer> teamNumbers = Queries.getTeamNumbersInEventDivision(connection, tournament,
                        division);
                final int numTeams = teamNumbers.size();
                final int hurdle = (int) Math.floor(numTeams * performanceHurdle);

                prep.setString(2, division);

                int count = 0;
                rs = prep.executeQuery();
                while (count < hurdle && rs.next()) {
                    final int teamNumber = rs.getInt(1);
                    bestTeams.add(teamNumber);
                    ++count;
                }
                SQLFunctions.close(rs);
                rs = null;
            } // foreach division

        } finally {
            SQLFunctions.close(rs);
            SQLFunctions.close(prep);
        }

        return bestTeams;
    }

    private static final Font ARIAL_8PT_BOLD = FontFactory.getFont(FontFactory.HELVETICA, 8, Font.BOLD);

    private static final Font ARIAL_8PT_NORMAL = FontFactory.getFont(FontFactory.HELVETICA, 8, Font.NORMAL);

    private static final Font ARIAL_8PT_NORMAL_RED = FontFactory.getFont(FontFactory.HELVETICA, 8, Font.NORMAL,
            BaseColor.RED);

    private static final Font TIMES_12PT_NORMAL = FontFactory.getFont(FontFactory.TIMES, 12, Font.NORMAL);

    /**
     * Generate the actual report.
     */
    private void generateReport(final Connection connection, final OutputStream out,
            final ChallengeDescription challengeDescription, final String challengeTitle,
            final Tournament tournament, final PdfPageEventHelper pageHandler, final Set<Integer> bestTeams)
            throws SQLException, IOException {
        if (tournament.getTournamentID() != Queries.getCurrentTournament(connection)) {
            throw new FLLRuntimeException(
                    "Cannot generate final score report for a tournament other than the current tournament");
        }

        final WinnerType winnerCriteria = challengeDescription.getWinner();

        final TournamentSchedule schedule;
        if (TournamentSchedule.scheduleExistsInDatabase(connection, tournament.getTournamentID())) {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Found a schedule for tournament: " + tournament);
            }
            schedule = new TournamentSchedule(connection, tournament.getTournamentID());
        } else {
            schedule = null;
        }

        try {
            // This creates our new PDF document and declares it to be in portrait
            // orientation
            final Document pdfDoc = PdfUtils.createPortraitPdfDoc(out, pageHandler);

            final Iterator<String> agIter = Queries.getAwardGroups(connection).iterator();
            while (agIter.hasNext()) {
                final String awardGroup = agIter.next();

                final ScoreCategory[] subjectiveCategories = challengeDescription.getSubjectiveCategories()
                        .toArray(new ScoreCategory[0]);

                // Figure out how many subjective categories have weights > 0.
                final double[] weights = new double[subjectiveCategories.length];
                int nonZeroWeights = 0;
                for (int catIndex = 0; catIndex < subjectiveCategories.length; catIndex++) {
                    weights[catIndex] = subjectiveCategories[catIndex].getWeight();
                    if (weights[catIndex] > 0.0) {
                        nonZeroWeights++;
                    }
                }
                // Array of relative widths for the columns of the score page
                // Array length varies with the number of subjective scores weighted >
                // 0.
                final int numColumnsLeftOfSubjective = 3;
                final int numColumnsRightOfSubjective = 2;
                final float[] relativeWidths = new float[numColumnsLeftOfSubjective + nonZeroWeights
                        + numColumnsRightOfSubjective];
                relativeWidths[0] = 3f;
                relativeWidths[1] = 1.0f;
                relativeWidths[2] = 1.0f;
                relativeWidths[relativeWidths.length - numColumnsRightOfSubjective] = 1.5f;
                relativeWidths[relativeWidths.length - numColumnsRightOfSubjective + 1] = 1.5f;
                for (int i = numColumnsLeftOfSubjective; i < numColumnsLeftOfSubjective + nonZeroWeights; i++) {
                    relativeWidths[i] = 1.5f;
                }

                // Create a table to hold all the scores for this division
                final PdfPTable divTable = new PdfPTable(relativeWidths);
                divTable.getDefaultCell().setBorder(0);
                divTable.setWidthPercentage(100);

                final PdfPTable header = createHeader(challengeTitle, tournament.getName(), awardGroup);
                final PdfPCell headerCell = new PdfPCell(header);
                headerCell.setColspan(relativeWidths.length);
                divTable.addCell(headerCell);

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("num relative widths: " + relativeWidths.length);
                    for (int i = 0; i < relativeWidths.length; ++i) {
                        LOGGER.trace("\twidth[" + i + "] = " + relativeWidths[i]);
                    }
                }

                writeColumnHeaders(schedule, weights, subjectiveCategories, relativeWidths, challengeDescription,
                        divTable);

                writeScores(connection, subjectiveCategories, weights, relativeWidths, awardGroup, winnerCriteria,
                        tournament, divTable, bestTeams);

                // Add the division table to the document
                pdfDoc.add(divTable);

                // If there is another division to process, start it on a new page
                if (agIter.hasNext()) {
                    pdfDoc.newPage();
                }
            }

            pdfDoc.close();
        } catch (final ParseException pe) {
            throw new RuntimeException("Error parsing category weight!", pe);
        } catch (final DocumentException de) {
            throw new RuntimeException("Error creating PDF document!", de);
        }
    }

    /**
     * @return {team number -> rank}
     * @throws SQLException
     */
    @SuppressFBWarnings(value = {
            "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" }, justification = "Winner criteria determines the sort")
    private Map<Integer, Integer> gatherRankedPerformanceTeams(final Connection connection,
            final WinnerType winnerCriteria, final Tournament tournament, final String awawrdGroup)
            throws SQLException {
        final Map<Integer, Integer> rankedTeams = new HashMap<>();

        PreparedStatement prep = null;
        ResultSet rs = null;
        try {
            prep = connection.prepareStatement(
                    "SELECT FinalScores.TeamNumber, FinalScores.performance" + " FROM FinalScores, TournamentTeams" //
                            + " WHERE FinalScores.Tournament = ?" //
                            + " AND TournamentTeams.Tournament = FinalScores.Tournament" //
                            + " AND TournamentTeams.event_division = ?"//
                            + " AND TournamentTeams.TeamNumber = FinalScores.TeamNumber"//
                            + " ORDER BY FinalScores.performance " + winnerCriteria.getSortString());
            prep.setInt(1, tournament.getTournamentID());
            prep.setString(2, awawrdGroup);

            int numTied = 1;
            int rank = 0;
            double prevScore = Double.NaN;
            rs = prep.executeQuery();
            while (rs.next()) {
                final int teamNumber = rs.getInt(1);
                double score = rs.getDouble(2);
                if (rs.wasNull()) {
                    score = Double.NaN;
                }

                if (!FP.equals(score, prevScore, 1E-6)) {
                    rank += numTied;
                    numTied = 1;
                } else {
                    ++numTied;
                }

                rankedTeams.put(teamNumber, rank);

                prevScore = score;
            }

        } finally {
            SQLFunctions.close(rs);
            SQLFunctions.close(prep);
        }

        return rankedTeams;
    }

    /**
     * @return category -> {Judging Group -> {team number -> rank}}
     * @throws SQLException
     */
    @SuppressFBWarnings(value = {
            "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" }, justification = "Category name and winner criteria determines the sort")
    private Map<ScoreCategory, Map<String, Map<Integer, Integer>>> gatherRankedSubjectiveTeams(
            final Connection connection, final ScoreCategory[] subjectiveCategories,
            final WinnerType winnerCriteria, final Tournament tournament, final String awardGroup)
            throws SQLException {
        final Map<ScoreCategory, Map<String, Map<Integer, Integer>>> retval = new HashMap<>();
        final List<String> judgingStations = Queries.getJudgingStations(connection, tournament.getTournamentID());

        for (int cat = 0; cat < subjectiveCategories.length; cat++) {
            final String catName = subjectiveCategories[cat].getName();

            final Map<String, Map<Integer, Integer>> categoryRanks = new HashMap<>();

            try (final PreparedStatement prep = connection
                    .prepareStatement("SELECT FinalScores.TeamNumber, FinalScores." + catName //
                            + " FROM FinalScores, TournamentTeams" //
                            + " WHERE FinalScores.Tournament = ?" //
                            + " AND TournamentTeams.Tournament = FinalScores.Tournament" //
                            + " AND TournamentTeams.event_division = ?"//
                            + " AND TournamentTeams.TeamNumber = FinalScores.TeamNumber"//
                            + " AND TournamentTeams.judging_station = ?" //
                            + " ORDER BY FinalScores." + catName + " " + winnerCriteria.getSortString())) {
                prep.setInt(1, tournament.getTournamentID());
                prep.setString(2, awardGroup);

                for (final String judgingStation : judgingStations) {
                    prep.setString(3, judgingStation);

                    final Map<Integer, Integer> rankedTeams = new HashMap<>();

                    int numTied = 1;
                    int rank = 0;
                    double prevScore = Double.NaN;
                    try (final ResultSet rs = prep.executeQuery()) {
                        while (rs.next()) {
                            final int teamNumber = rs.getInt(1);
                            double score = rs.getDouble(2);
                            if (rs.wasNull()) {
                                score = Double.NaN;
                            }

                            if (!FP.equals(score, prevScore, 1E-6)) {
                                rank += numTied;
                                numTied = 1;
                            } else {
                                ++numTied;
                            }

                            rankedTeams.put(teamNumber, rank);

                            prevScore = score;
                        }

                        categoryRanks.put(judgingStation, rankedTeams);

                    } // try ResultSet
                } // foreach judging station

                retval.put(subjectiveCategories[cat], categoryRanks);

            } // try PreparedStatement
        } // foreach category

        return retval;

    }

    @SuppressFBWarnings(value = {
            "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" }, justification = "Category name determines table name")
    private void writeScores(final Connection connection, final ScoreCategory[] subjectiveCategories,
            final double[] weights, final float[] relativeWidths, final String awardGroup,
            final WinnerType winnerCriteria, final Tournament tournament, final PdfPTable divTable,
            final Set<Integer> bestTeams) throws SQLException {

        final Map<ScoreCategory, Map<String, Map<Integer, Integer>>> teamSubjectiveRanks = gatherRankedSubjectiveTeams(
                connection, subjectiveCategories, winnerCriteria, tournament, awardGroup);

        final Map<Integer, Integer> teamPerformanceRanks = gatherRankedPerformanceTeams(connection, winnerCriteria,
                tournament, awardGroup);

        ResultSet rawScoreRS = null;
        PreparedStatement teamPrep = null;
        ResultSet teamsRS = null;
        PreparedStatement scorePrep = null;
        try {
            final StringBuilder query = new StringBuilder();
            query.append(
                    "SELECT Teams.Organization,Teams.TeamName,Teams.TeamNumber,FinalScores.OverallScore,FinalScores.performance,current_tournament_teams.judging_station");
            for (int cat = 0; cat < subjectiveCategories.length; cat++) {
                if (weights[cat] > 0.0) {
                    final String catName = subjectiveCategories[cat].getName();
                    query.append(",FinalScores." + catName);
                }
            }
            query.append(" FROM Teams,FinalScores,current_tournament_teams");
            query.append(" WHERE FinalScores.TeamNumber = Teams.TeamNumber");
            query.append(" AND FinalScores.Tournament = ?");
            query.append(" AND current_tournament_teams.event_division = ?");
            query.append(" AND current_tournament_teams.TeamNumber = Teams.TeamNumber");
            query.append(
                    " ORDER BY FinalScores.OverallScore " + winnerCriteria.getSortString() + ", Teams.TeamNumber");
            teamPrep = connection.prepareStatement(query.toString());
            teamPrep.setInt(1, tournament.getTournamentID());
            teamPrep.setString(2, awardGroup);
            teamsRS = teamPrep.executeQuery();

            scorePrep = connection
                    .prepareStatement("SELECT score FROM performance_seeding_max" + " WHERE TeamNumber = ?");

            while (teamsRS.next()) {
                final int teamNumber = teamsRS.getInt(3);
                final String organization = teamsRS.getString(1);
                final String teamName = teamsRS.getString(2);
                final String judgingGroup = teamsRS.getString(6);

                final double totalScore;
                final double ts = teamsRS.getDouble(4);
                if (teamsRS.wasNull()) {
                    totalScore = Double.NaN;
                } else {
                    totalScore = ts;
                }

                // ///////////////////////////////////////////////////////////////////
                // Build a table of data for this team
                // ///////////////////////////////////////////////////////////////////
                final PdfPTable curteam = new PdfPTable(relativeWidths);
                curteam.getDefaultCell().setBorder(0);

                // The first row of the team table...
                // First column is organization name
                final PdfPCell teamCol = new PdfPCell(new Phrase(organization, ARIAL_8PT_NORMAL));
                teamCol.setBorder(0);
                teamCol.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_LEFT);
                curteam.addCell(teamCol);
                curteam.getDefaultCell().setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_RIGHT);

                final PdfPCell judgeGroupCell = new PdfPCell(new Phrase(judgingGroup, ARIAL_8PT_NORMAL));
                judgeGroupCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                judgeGroupCell.setBorder(0);
                curteam.addCell(judgeGroupCell);

                // Second column is "Raw:"
                final PdfPCell rawLabel = new PdfPCell(new Phrase("Raw:", ARIAL_8PT_NORMAL));
                rawLabel.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_RIGHT);
                rawLabel.setBorder(0);
                curteam.addCell(rawLabel);

                insertRawScoreColumns(connection, tournament, winnerCriteria.getSortString(), subjectiveCategories,
                        weights, teamNumber, curteam);

                // Column for the highest performance score of the seeding rounds
                scorePrep.setInt(1, teamNumber);
                rawScoreRS = scorePrep.executeQuery();
                final double rawScore;
                if (rawScoreRS.next()) {
                    final double v = rawScoreRS.getDouble(1);
                    if (rawScoreRS.wasNull()) {
                        rawScore = Double.NaN;
                    } else {
                        rawScore = v;
                    }
                } else {
                    rawScore = Double.NaN;
                }
                PdfPCell pCell = new PdfPCell((Double.isNaN(rawScore) ? new Phrase("No Score", ARIAL_8PT_NORMAL_RED)
                        : new Phrase(Utilities.NUMBER_FORMAT_INSTANCE.format(rawScore), ARIAL_8PT_NORMAL)));
                pCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                pCell.setBorder(0);
                curteam.addCell(pCell);
                rawScoreRS.close();

                // The "Overall score" column is not filled in for raw scores
                curteam.addCell("");

                // The second row of the team table...
                // First column contains the team # and name
                final PdfPCell teamNameCol = new PdfPCell(
                        new Phrase(Integer.toString(teamNumber) + " " + teamName, ARIAL_8PT_NORMAL));
                teamNameCol.setBorder(0);
                teamNameCol.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_LEFT);
                curteam.addCell(teamNameCol);

                // Second column contains "Scaled:"
                final PdfPCell scaledCell = new PdfPCell(new Phrase("Scaled:", ARIAL_8PT_NORMAL));
                scaledCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_RIGHT);
                scaledCell.setBorder(0);
                scaledCell.setColspan(2);
                curteam.addCell(scaledCell);

                // Next, one column containing the scaled score for each subjective
                // category with weight > 0
                for (int cat = 0; cat < subjectiveCategories.length; cat++) {
                    final Map<String, Map<Integer, Integer>> catRanks = teamSubjectiveRanks
                            .get(subjectiveCategories[cat]);

                    final double catWeight = weights[cat];
                    if (catWeight > 0.0) {
                        final double scaledScore;
                        final double v = teamsRS.getDouble(6 + cat + 1);
                        if (teamsRS.wasNull()) {
                            scaledScore = Double.NaN;
                        } else {
                            scaledScore = v;
                        }

                        final Map<Integer, Integer> judgingRanks = catRanks.get(judgingGroup);

                        Font scoreFont;
                        final String rankText;
                        if (judgingRanks.containsKey(teamNumber)) {
                            final int rank = judgingRanks.get(teamNumber);
                            rankText = String.format("%1$s(%2$d)", Utilities.NON_BREAKING_SPACE, rank);
                            if (1 == rank) {
                                scoreFont = ARIAL_8PT_BOLD;
                            } else {
                                scoreFont = ARIAL_8PT_NORMAL;
                            }
                        } else {
                            rankText = String.format("%1$s%1$s%1$s%1$s%1$s", Utilities.NON_BREAKING_SPACE);
                            scoreFont = ARIAL_8PT_NORMAL;
                        }

                        final String scoreText;
                        if (Double.isNaN(scaledScore)) {
                            scoreText = "No Score" + rankText;
                            scoreFont = ARIAL_8PT_NORMAL_RED;
                        } else {
                            scoreText = Utilities.NUMBER_FORMAT_INSTANCE.format(scaledScore) + rankText;
                        }

                        final PdfPCell subjCell = new PdfPCell(new Phrase(scoreText, scoreFont));
                        subjCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                        subjCell.setBorder(0);
                        curteam.addCell(subjCell);
                    }
                } // foreach category

                // 2nd to last column has the scaled performance score
                {
                    Font scoreFont;
                    final String rankText;
                    if (teamPerformanceRanks.containsKey(teamNumber)) {
                        final int rank = teamPerformanceRanks.get(teamNumber);
                        rankText = String.format("%1$s(%2$d)", Utilities.NON_BREAKING_SPACE, rank);
                        if (1 == rank) {
                            scoreFont = ARIAL_8PT_BOLD;
                        } else {
                            scoreFont = ARIAL_8PT_NORMAL;
                        }
                    } else {
                        rankText = String.format("%1$s%1$s%1$s%1$s%1$s", Utilities.NON_BREAKING_SPACE);
                        scoreFont = ARIAL_8PT_NORMAL;
                    }

                    final double scaledScore;
                    final double v = teamsRS.getDouble(5);
                    if (teamsRS.wasNull()) {
                        scaledScore = Double.NaN;
                    } else {
                        scaledScore = v;
                    }

                    final String scaledScoreStr;
                    if (Double.isNaN(scaledScore)) {
                        scoreFont = ARIAL_8PT_NORMAL_RED;
                        scaledScoreStr = "No Score" + rankText;
                    } else {
                        scaledScoreStr = Utilities.NUMBER_FORMAT_INSTANCE.format(scaledScore) + rankText;
                    }

                    pCell = new PdfPCell(new Phrase(scaledScoreStr, scoreFont));
                    pCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                    pCell.setBorder(0);
                    curteam.addCell(pCell);
                } // performance score

                // Last column contains the overall scaled score
                final String overallScoreSuffix;
                if (bestTeams.contains(teamNumber)) {
                    overallScoreSuffix = String.format("%1$s*", Utilities.NON_BREAKING_SPACE);
                } else {
                    overallScoreSuffix = String.format("%1$s%1$s", Utilities.NON_BREAKING_SPACE);
                }

                pCell = new PdfPCell((Double.isNaN(totalScore)
                        ? new Phrase("No Score" + overallScoreSuffix, ARIAL_8PT_NORMAL_RED)
                        : new Phrase(Utilities.NUMBER_FORMAT_INSTANCE.format(totalScore) + overallScoreSuffix,
                                ARIAL_8PT_NORMAL)));
                pCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                pCell.setBorder(0);
                curteam.addCell(pCell);

                // This is an empty row in the team table that is added to put a
                // horizontal rule under the team's score in the display
                final PdfPCell blankCell = new PdfPCell();
                blankCell.setBorder(0);
                blankCell.setBorderWidthBottom(0.5f);
                blankCell.setBorderColorBottom(BaseColor.GRAY);
                blankCell.setColspan(relativeWidths.length);
                curteam.addCell(blankCell);

                // Create a new cell and add it to the division table - this cell will
                // contain the entire team table we just built above
                final PdfPCell curteamCell = new PdfPCell(curteam);
                curteamCell.setBorder(0);
                curteamCell.setColspan(relativeWidths.length);
                divTable.addCell(curteamCell);
            }

            teamsRS.close();

        } finally {
            SQLFunctions.close(teamsRS);
            SQLFunctions.close(teamPrep);
            SQLFunctions.close(rawScoreRS);
            SQLFunctions.close(scorePrep);
        }

    }

    /**
     * @throws ParseException
     */
    private void writeColumnHeaders(final TournamentSchedule schedule, final double[] weights,
            final ScoreCategory[] subjectiveCategories, final float[] relativeWidths,
            final ChallengeDescription challengeDescription, final PdfPTable divTable) throws ParseException {

        // /////////////////////////////////////////////////////////////////////
        // Write the table column headers
        // /////////////////////////////////////////////////////////////////////
        // team information
        final PdfPCell organizationCell = new PdfPCell(new Phrase("Organization", ARIAL_8PT_BOLD));
        organizationCell.setBorder(0);
        organizationCell.setVerticalAlignment(com.itextpdf.text.Element.ALIGN_MIDDLE);
        divTable.addCell(organizationCell);

        // judging group
        if (null != schedule) {
            final Paragraph judgingGroup = new Paragraph("Judging", ARIAL_8PT_BOLD);
            judgingGroup.add(Chunk.NEWLINE);
            judgingGroup.add(new Chunk("Group"));
            final PdfPCell osCell = new PdfPCell(judgingGroup);
            osCell.setBorder(0);
            osCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
            osCell.setVerticalAlignment(com.itextpdf.text.Element.ALIGN_MIDDLE);
            divTable.addCell(osCell);
        }

        divTable.addCell(""); // weight/raw&scaled

        for (int cat = 0; cat < subjectiveCategories.length; cat++) {
            if (weights[cat] > 0.0) {
                final String catTitle = subjectiveCategories[cat].getTitle();

                final Paragraph catPar = new Paragraph(catTitle, ARIAL_8PT_BOLD);
                final PdfPCell catCell = new PdfPCell(catPar);
                catCell.setBorder(0);
                catCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                catCell.setVerticalAlignment(com.itextpdf.text.Element.ALIGN_MIDDLE);
                divTable.addCell(catCell);
            }
        }

        final Paragraph perfPar = new Paragraph("Performance", ARIAL_8PT_BOLD);
        final PdfPCell perfCell = new PdfPCell(perfPar);
        perfCell.setBorder(0);
        perfCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
        perfCell.setVerticalAlignment(com.itextpdf.text.Element.ALIGN_MIDDLE);
        divTable.addCell(perfCell);

        final Paragraph overallScore = new Paragraph("Overall", ARIAL_8PT_BOLD);
        overallScore.add(Chunk.NEWLINE);
        overallScore.add(new Chunk("Score"));
        final PdfPCell osCell = new PdfPCell(overallScore);
        osCell.setBorder(0);
        osCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
        osCell.setVerticalAlignment(com.itextpdf.text.Element.ALIGN_MIDDLE);
        divTable.addCell(osCell);

        // /////////////////////////////////////////////////////////////////////
        // Write a table row with the relative weights of the subjective scores
        // /////////////////////////////////////////////////////////////////////

        final PdfPCell teamCell = new PdfPCell(new Phrase("Team # / Team Name", ARIAL_8PT_BOLD));
        teamCell.setBorder(0);
        teamCell.setVerticalAlignment(com.itextpdf.text.Element.ALIGN_MIDDLE);
        divTable.addCell(teamCell);

        final Paragraph wPar = new Paragraph("Weight:", ARIAL_8PT_NORMAL);
        final PdfPCell wCell = new PdfPCell(wPar);
        if (null != schedule) {
            wCell.setColspan(2);
        }
        wCell.setBorder(0);
        wCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_RIGHT);
        divTable.addCell(wCell);

        final PdfPCell[] wCells = new PdfPCell[subjectiveCategories.length];
        final Paragraph[] wPars = new Paragraph[subjectiveCategories.length];
        for (int cat = 0; cat < subjectiveCategories.length; cat++) {
            if (weights[cat] > 0.0) {
                wPars[cat] = new Paragraph(Double.toString(weights[cat]), ARIAL_8PT_NORMAL);
                wCells[cat] = new PdfPCell(wPars[cat]);
                wCells[cat].setBorder(0);
                wCells[cat].setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                divTable.addCell(wCells[cat]);
            }
        }

        final PerformanceScoreCategory performanceElement = challengeDescription.getPerformance();
        final double perfWeight = performanceElement.getWeight();
        final Paragraph perfWeightPar = new Paragraph(Double.toString(perfWeight), ARIAL_8PT_NORMAL);
        final PdfPCell perfWeightCell = new PdfPCell(perfWeightPar);
        perfWeightCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
        perfWeightCell.setBorder(0);
        divTable.addCell(perfWeightCell);

        divTable.addCell("");

        PdfPCell blankCell = new PdfPCell();
        blankCell.setBorder(0);
        blankCell.setBorderWidthBottom(1.0f);
        blankCell.setColspan(relativeWidths.length);
        divTable.addCell(blankCell);

        // Cause the first 4 rows to be repeated on
        // each page - 1 row for box header, 2 rows text headers and 1 for
        // the horizontal line.
        divTable.setHeaderRows(4);
    }

    @SuppressFBWarnings(value = {
            "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" }, justification = "Winner type is used to determine sort order")
    private void insertRawScoreColumns(final Connection connection, final Tournament tournament,
            final String ascDesc, final ScoreCategory[] subjectiveCategories, final double[] weights,
            final int teamNumber, final PdfPTable curteam) throws SQLException {
        ResultSet rs = null;
        PreparedStatement prep = null;
        try {
            // Next, one column containing the raw score for each subjective
            // category with weight > 0
            for (int catIndex = 0; catIndex < subjectiveCategories.length; catIndex++) {
                final ScoreCategory catElement = subjectiveCategories[catIndex];
                final double catWeight = weights[catIndex];
                if (catWeight > 0.0) {
                    final String catName = catElement.getName();
                    prep = connection.prepareStatement("SELECT ComputedTotal" + " FROM " + catName
                            + " WHERE TeamNumber = ? AND Tournament = ? ORDER BY ComputedTotal " + ascDesc);
                    prep.setInt(1, teamNumber);
                    prep.setInt(2, tournament.getTournamentID());
                    rs = prep.executeQuery();
                    boolean scoreSeen = false;
                    final StringBuilder rawScoreText = new StringBuilder();
                    while (rs.next()) {
                        final double v = rs.getDouble(1);
                        if (!rs.wasNull()) {
                            if (scoreSeen) {
                                rawScoreText.append(", ");
                            } else {
                                scoreSeen = true;
                            }
                            rawScoreText.append(Utilities.NUMBER_FORMAT_INSTANCE.format(v));
                        }
                    }
                    final PdfPCell subjCell = new PdfPCell(
                            (!scoreSeen ? new Phrase("No Score", ARIAL_8PT_NORMAL_RED)
                                    : new Phrase(rawScoreText.toString(), ARIAL_8PT_NORMAL)));
                    subjCell.setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_CENTER);
                    subjCell.setBorder(0);
                    curteam.addCell(subjCell);
                    rs.close();
                }
            }
        } finally {
            SQLFunctions.close(rs);
            SQLFunctions.close(prep);
        }
    }

    private PdfPTable createHeader(final String challengeTitle, final String tournamentName,
            final String division) {
        // initialization of the header table
        final PdfPTable header = new PdfPTable(2);

        final Phrase p = new Phrase();
        p.add(new Chunk(challengeTitle, TIMES_12PT_NORMAL));
        p.add(Chunk.NEWLINE);
        p.add(new Chunk("Final Computed Scores", TIMES_12PT_NORMAL));
        header.getDefaultCell().setBorderWidth(0);
        header.addCell(p);
        header.getDefaultCell().setHorizontalAlignment(com.itextpdf.text.Element.ALIGN_RIGHT);

        final Phrase p2 = new Phrase();
        p2.add(new Chunk("Tournament: " + tournamentName, TIMES_12PT_NORMAL));
        p2.add(Chunk.NEWLINE);
        p2.add(new Chunk("Award Group: " + division, TIMES_12PT_NORMAL));
        header.addCell(p2);

        return header;
    }

    private static class FooterHandler extends PdfPageEventHelper {

        private PdfTemplate _tpl;

        private BaseFont _headerFooterFont;

        private final String _legendText;

        /**
         * @param percentageHurdle percentage as an integer between 0 and 100
         */
        public FooterHandler(final int percentageHurdle) {
            if (percentageHurdle > 0 && percentageHurdle < 100) {
                _legendText = String.format(
                        "* - teams in the top %d%% of performance scores, bold - top team in a category & judging group",
                        percentageHurdle);
            } else {
                _legendText = "bold - top team in a category & judging group";
            }
        }

        @Override
        public void onOpenDocument(final PdfWriter writer, final Document document) {
            _headerFooterFont = TIMES_12PT_NORMAL.getBaseFont();

            // initialization of the footer template
            _tpl = writer.getDirectContent().createTemplate(100, 100);
            _tpl.setBoundingBox(new Rectangle(-20, -20, 100, 100));
        }

        @Override
        public void onEndPage(final PdfWriter writer, final Document document) {
            final PdfContentByte cb = writer.getDirectContent();
            cb.saveState();

            // compose the footer

            final float textSize = _headerFooterFont.getWidthPoint(_legendText, 12);
            final float textBase = document.bottom() - 20;
            cb.beginText();
            cb.setFontAndSize(_headerFooterFont, 12);

            final float adjust = _headerFooterFont.getWidthPoint("0", 12);
            cb.setTextMatrix(document.right() - textSize - adjust, textBase);
            cb.showText(_legendText);
            cb.endText();
            cb.addTemplate(_tpl, document.right() - adjust, textBase);

            cb.restoreState();
        }

    } // class FooterHandler

} // class FinalComputedScores