Context:
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010-2014 Chris Nelson <Chris.Nelson@SIXNET.com>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.

import re
import time
from datetime import timedelta, datetime
from operator import itemgetter, attrgetter

from trac.util.datefmt import localtz
try:
    from trac.util.datefmt import to_utimestamp
except ImportError:
    from trac.util.datefmt import to_timestamp as to_utimestamp
from trac.util.text import to_unicode
from trac.util.html import Markup
from trac.wiki.macros import WikiMacroBase
from trac.web.chrome import Chrome
import copy
from trac.ticket.query import Query

from trac.config import IntOption, Option
from trac.core import implements, Component, TracError
from trac.web.api import IRequestFilter
from trac.web.chrome import ITemplateProvider, add_script, add_stylesheet
from pkg_resources import resource_filename

from trac.wiki.api import parse_args

from tracpm import TracPM

try:
    from trac.util.text import javascript_quote
except ImportError:
    # Fallback for Trac<0.11.3 - verbatim copy from Trac 1.0
    _js_quote = {'\\': '\\\\', '"': '\\"', '\b': '\\b', '\f': '\\f',
                 '\n': '\\n', '\r': '\\r', '\t': '\\t', "'": "\\'"}
    for i in range(0x20) + [ord(c) for c in '&<>']:
        _js_quote.setdefault(chr(i), '\\u%04x' % i)
    _js_quote_re = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t\'&<>]')

    def javascript_quote(text):
        """Quote strings for inclusion in javascript"""
        if not text:
            return ''
        def replace(match):
            return _js_quote[match.group(0)]
        return _js_quote_re.sub(replace, text)

# ========================================================================
class TracJSGanttSupport(Component):
    implements(IRequestFilter, ITemplateProvider)

    Option('trac-jsgantt', 'option.format', 'day',
           """Initial format of Gantt chart""")
    Option('trac-jsgantt', 'option.formats', 'day|week|month|quarter',
           """Formats to show for Gantt chart""")
    IntOption('trac-jsgantt', 'option.sample', 0,
              """Show sample Gantt""")
    IntOption('trac-jsgantt', 'option.res', 1,
              """Show resource column""")
    IntOption('trac-jsgantt', 'option.dur', 1,
              """Show duration column""")
    IntOption('trac-jsgantt', 'option.comp', 1,
              """Show percent complete column""")
    Option('trac-jsgantt', 'option.caption', 'Resource',
           """Caption to follow task in Gantt""")
    IntOption('trac-jsgantt', 'option.startDate', 1,
              """Show start date column""")
    IntOption('trac-jsgantt', 'option.endDate', 1,
              """Show finish date column""")
    Option('trac-jsgantt', 'option.dateDisplay', 'mm/dd/yyyy',
           """Format to display dates""")
    IntOption('trac-jsgantt', 'option.openLevel', 999,
              """How many levels of task hierarchy to show open""")
    IntOption('trac-jsgantt', 'option.expandClosedTickets', 1,
              """Show children of closed tasks in the task hierarchy""")
    Option('trac-jsgantt', 'option.colorBy', 'priority',
           """Field to use to color tasks""")
    IntOption('trac-jsgantt', 'option.lwidth', None,
              """Width (in pixels) of left table""")
    IntOption('trac-jsgantt', 'option.showdep', 1,
              """Show dependencies in Gantt""")
    IntOption('trac-jsgantt', 'option.userMap', 1,
              """Map user IDs to user names""")
    IntOption('trac-jsgantt', 'option.omitMilestones', 0,
              """Omit milestones""")
    Option('trac-jsgantt', 'option.schedule', 'alap',
           """Schedule algorithm: alap or asap""")
    IntOption('trac-jsgantt', 'option.doResourceLeveling', 0,
              """Resource level (1) or not (0)""")
    # This seems to be the first floating point option.
    Option('trac-jsgantt', 'option.hoursPerDay', '8.0',
                """Hours worked per day""")
    Option('trac-jsgantt', 'option.display', None,
                """Display filter for tickets in the form 'field1:value1|field2:value2' or 'field:value1|value2'; displays tickets where field1==value1, etc.""")
    Option('trac-jsgantt', 'option.order', 'wbs',
           """Fields to sort tasks by before display.  May include tickets fields (including custom fields) or 'wbs'.""")
    Option('trac-jsgantt', 'option.scrollTo', None,
           """Date to scroll chart to (yyyy-mm--dd or 'today')""")

    Option('trac-jsGantt', 'option.linkStyle', 'standard',
            """Style for ticket links; jsgantt (new window) or standard browser behavior like ticket links.""")

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        return [('tracjsgantt', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        return []

    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        # I think we should look for a TracJSGantt on the page and set
        # a flag for the post_process_request handler if found
        return handler

    def post_process_request(self, req, template, data, content_type):
        add_script(req, 'tracjsgantt/jsgantt.js')
        add_stylesheet(req, 'tracjsgantt/jsgantt.css')
        add_stylesheet(req, 'tracjsgantt/tracjsgantt.css')
        return template, data, content_type

class TracJSGanttChart(WikiMacroBase):
    """
Displays a Gantt chart for the specified tickets.

The chart display can be controlled with a number of macro arguments:

||'''Argument'''||'''Description'''||'''Default'''||
|| `formats`||What to display in the format control.  A pipe-separated list of `minute`, `hour`, `day`, `week`, `month`, and `quarter` (though `minute` may not be very useful). ||'day|week|month|quarter'||
|| `format`||Initial display format, one of those listed in `formats` || First format ||
|| `sample`||Display sample tasks (1) or not (0) || 0 ||
|| `res`||Show resource column (1) or not (0) || 1 ||
|| `dur`||Show duration colunn (1) or not (0) || 1 ||
|| `comp`||Show percent complete column (1) or not (0) || 1 ||
|| `caption`||Caption to place to right of tasks: None, Caption, Resource, Duration, %Complete || Resource ||
|| `startDate`||Show start date column (1) or not (0) || 1 ||
|| `endDate`||Show end date column (1) or not (0) || 1 ||
|| `dateDisplay`||Date display format: 'mm/dd/yyyy', 'dd/mm/yyyy', or 'yyyy-mm-dd' || 'mm/dd/yyyy' ||
|| `openLevel`||Number of levels of tasks to show.  1 = only top level task.  || 999 ||
|| `colorBy`||Field to use to choose task colors.  Each unique value of the field will have a different color task.  Other likely useful values are owner and milestone but any field can be used. || priority ||
|| `root`||When using something like Subtickets plugin to maintain a tree of tickets and subtickets, you may create a Gantt showing a ticket and all of its descendants with `root=<ticket#>`.  The macro uses the configured `parent` field to find all descendant tasks and build an `id=` argument for Trac's native query handler.[[br]][[br]]Multiple roots may be provided like `root=1|12|32`.[[br]][[br]]When used in a ticket description or comment, `root=self` will display the current ticket's descendants.||None||
|| `goal`||When using something like MasterTickets plugin to maintain ticket dependencies, you may create a Gantt showing a ticket and all of its predecessors with `goal=<ticket#>`.  The macro uses the configured `succ` field to find all predecessor tasks and build an `id=` argument for Trac's native query handler.[[br]][[br]]Multiple goals may be provided like `goal=1|12|32`.[[br]][[br]]When used in a ticket description or comment, `goal=self` will display the current ticket's predecessors.||None||
|| `lwidth`||The width, in pixels, of the table of task names, etc. on the left of the Gantt. || ||
|| `showdep`||Show dependencies (1) or not (0)||1||
|| `userMap`||Map user !IDs to full names (1) or not (0).||1||
|| `omitMilestones`||Show milestones for displayed tickets (0) or only those specified by `milestone=` (1)||0||
|| `schedule`||Schedule tasks based on dependenies and estimates.  Either as soon as possible (asap) or as late as possible (alap)||alap||
||`doResourceLeveling`||Resolve resource conflicts (1) or not (0) when scheduling tickets.||0||
||`display`||Filter for limiting display of tickets.  `owner:fred` shows only tickets owned by fred. `status:closed` shows only closed tickets.||None||
||`order`||Order of fields used to sort tickets before display. `order=milestone` sorts by milestone.  May include ticket fields, including custom fields, or "wbs" (work breakdown structure).||wbs||

Site-wide defaults for macro arguments may be set in the `trac-jsgantt` section of `trac.ini`.  `option.<opt>` overrides the built-in default for `<opt>` from the table above.

All other macro arguments are treated as TracQuery specification (e.g., milestone=ms1|ms2) to control which tickets are displayed.

    """

    pm = None
    options = {}

    # The date part of these formats has to be in sync.  Including
    # hour and minute in the pyDateFormat makes the plugin easier to
    # debug at times because that's how the date shows up in page
    # source.
    #
    # jsDateFormat is the date format that the JavaScript expects
    # dates in.  It can be one of 'mm/dd/yyyy', 'dd/mm/yyyy', or
    # 'yyyy-mm-dd'.  pyDateFormat is a strptime() format that matches
    # jsDateFormat.  As long as they are in sync, there's no real
    # reason to change them.
    jsDateFormat = 'yyyy-mm-dd'
    pyDateFormat = '%Y-%m-%d %H:%M'

    # User map (login -> realname) is loaded on demand, once.
    # Initialization to None means it is not yet initialized.
    user_map = None

    def __init__(self):
        # Instantiate the PM component
        self.pm = TracPM(self.env)

        self.GanttID = 'g'

        # All the macro's options with default values.
        # Anything else passed to the macro is a TracQuery field.
        options = ('format', 'formats', 'sample', 'res', 'dur', 'comp',
                   'caption', 'startDate', 'endDate', 'dateDisplay',
                   'openLevel', 'expandClosedTickets', 'colorBy', 'lwidth',
                   'showdep', 'userMap', 'omitMilestones',
                   'schedule', 'hoursPerDay', 'doResourceLeveling',
                   'display', 'order', 'scrollTo', 'linkStyle')

        for opt in options:
            self.options[opt] = self.config.get('trac-jsgantt',
                                                'option.%s' % opt)

    def _begin_gantt(self, options):
        if options['format']:
            defaultFormat = options['format']
        else:
            defaultFormat = options['formats'].split('|')[0]
        showdep = options['showdep']
        text = ''
        text += '

\n'
        text += '\n'
        return chart

    def _gantt_options(self, options):
        opt = ''
        if (options['linkStyle']):
            linkStyle = options['linkStyle']
        else:
            linkStyle = 'standard'
        opt += self.GanttID+'.setLinkStyle("%s")\n' % linkStyle
        opt += self.GanttID+'.setShowRes(%s);\n' % options['res']
        opt += self.GanttID+'.setShowDur(%s);\n' % options['dur']
        opt += self.GanttID+'.setShowComp(%s);\n' % options['comp']
        if (options['scrollTo']):
            opt += self.GanttID+'.setScrollDate("%s");\n' % options['scrollTo']
        w = options['lwidth']
        if w:
            opt += self.GanttID+'.setLeftWidth(%s);\n' % w

        opt += self.GanttID+'.setCaptionType("%s");\n' % \
            javascript_quote(options['caption'])

        opt += self.GanttID+'.setShowStartDate(%s);\n' % options['startDate']
        opt += self.GanttID+'.setShowEndDate(%s);\n' % options['endDate']

        opt += self.GanttID+'.setDateInputFormat("%s");\n' % \
            javascript_quote(self.jsDateFormat)

        opt += self.GanttID+'.setDateDisplayFormat("%s");\n' % \
            javascript_quote(options['dateDisplay'])

        opt += self.GanttID+'.setFormatArr(%s);\n' % ','.join(
            '"%s"' % javascript_quote(f) for f in options['formats'].split('|'))
        opt += self.GanttID+'.setPopupFeatures("location=1,scrollbars=1");\n'
        return opt

    # TODO - use ticket-classN styles instead of colors?
    def _add_sample_tasks(self):
        task= ''
        tasks = self.GanttID+'.setDateInputFormat("mm/dd/yyyy");\n'

        #                                                                         ID    Name                   Start        End          Display    Link                    MS Res         Pct  Gr Par Open Dep Cap
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',1,   "Define Chart API",     "",          "",          "#ff0000", "http://help.com",      0, "Brian",     0,  1, 0,  1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',11,  "Chart Object",         "2/20/2011", "2/20/2011", "#ff00ff", "http://www.yahoo.com", 1, "Shlomy",  100,  0, 1,  1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',12,  "Task Objects",         "",          "",          "#00ff00", "",                     0, "Shlomy",   40,  1, 1,  1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',121, "Constructor Proc",     "2/21/2011", "3/9/2011",  "#00ffff", "http://www.yahoo.com", 0, "Brian T.", 60,  0, 12, 1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',122, "Task Variables",       "3/6/2011",  "3/11/2011", "#ff0000", "http://help.com",      0, "",         60,  0, 12, 1,121));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',123, "Task Functions",       "3/9/2011",  "3/29/2011", "#ff0000", "http://help.com",      0, "Anyone",   60,  0, 12, 1, 0, "This is another caption"));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',2,   "Create HTML Shell",    "3/24/2011", "3/25/2011", "#ffff00", "http://help.com",      0, "Brian",    20,  0, 0,  1,122));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',3,   "Code Javascript",      "",          "",          "#ff0000", "http://help.com",      0, "Brian",     0,  1, 0,  1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',31,  "Define Variables",     "2/25/2011", "3/17/2011", "#ff00ff", "http://help.com",      0, "Brian",    30,  0, 3,  1, 0,"Caption 1"));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',32,  "Calculate Chart Size", "3/15/2011", "3/24/2011", "#00ff00", "http://help.com",      0, "Shlomy",   40,  0, 3,  1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',33,  "Draw Taks Items",      "",          "",          "#00ff00", "http://help.com",      0, "Someone",  40,  1, 3,  1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',332, "Task Label Table",     "3/6/2011",  "3/11/2011", "#0000ff", "http://help.com",      0, "Brian",    60,  0, 33, 1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',333, "Task Scrolling Grid",  "3/9/2011",  "3/20/2011", "#0000ff", "http://help.com",      0, "Brian",    60,  0, 33, 1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',34,  "Draw Task Bars",       "",          "",          "#990000", "http://help.com",      0, "Anybody",  60,  1, 3,  1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',341, "Loop each Task",       "3/26/2011", "4/11/2011", "#ff0000", "http://help.com",      0, "Brian",    60,  0, 34, 1, "332,333"));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',342, "Calculate Start/Stop", "4/12/2011", "5/18/2011", "#ff6666", "http://help.com",      0, "Brian",    60,  0, 34, 1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',343, "Draw Task Div",        "5/13/2011", "5/17/2011", "#ff0000", "http://help.com",      0, "Brian",    60,  0, 34, 1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',344, "Draw Completion Div",  "5/17/2011", "6/04/2011", "#ff0000", "http://help.com",      0, "Brian",    60,  0, 34, 1));\n'
        tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem('+self.GanttID+',35,  "Make Updates",         "10/17/2011","12/04/2011","#f600f6", "http://help.com",      0, "Brian",    30,  0, 3,  1));\n'
        return tasks

    # Get the required columns for the tickets which match the
    # criteria in options.
    def _query_tickets(self, options):
        query_options = {}
        for key in options.keys():
            if not key in self.options:
                query_options[key] = options[key]

        # The fields always needed by the Gantt
        fields = set([
            'description',
            'owner',
            'type',
            'status',
            'summary',
            'milestone',
            'priority'])

        # Make sure the coloring field is included
        if 'colorBy' in options:
            fields.add(str(options['colorBy']))

        rawtickets = self.pm.query(query_options, fields, self.req)

        # Do permissions check on tickets
        tickets = [t for t in rawtickets
                   if 'TICKET_VIEW' in self.req.perm('ticket', t['id'])]

        return tickets

    def _compare_tickets(self, t1, t2):
        # If t2 depends on t1, t2 is first
        if t1['id'] in self.pm.successors(t2):
            return 1
        # If t1 depends on t2, t1 is first
        elif t2['id'] in self.pm.successors(t1):
            return -1
        # If t1 ends first, it's first
        elif self.pm.finish(t1) < self.pm.finish(t2):
            return -1
        # If t2 ends first, it's first
        elif self.pm.finish(t1) > self.pm.finish(t2):
            return 1
        # End dates are same. If t1 starts later, it's later
        elif self.pm.start(t1) > self.pm.start(t2):
            return 1
        # Otherwise, preserve order (assume t1 is before t2 when called)
        else:
            return 0

    # Compute WBS for sorting and figure out the tickets' levels for
    # controlling how many levels are open.
    #
    # WBS is a list like [ 2, 4, 1] (the first child of the fourth
    # child of the second top-level element).
    def _compute_wbs(self):
        # Set the ticket's level and wbs then recurse to children.
        def _setLevel(tid, wbs, level):
            # Update this node
            self.ticketsByID[tid]['level'] = level
            self.ticketsByID[tid]['wbs'] = copy.copy(wbs)

            # Recurse to children
            childIDs = self.pm.children(self.ticketsByID[tid])
            if childIDs:
                childTickets = [self.ticketsByID[cid] for cid in childIDs]
                childTickets.sort(self._compare_tickets)
                childIDs = [ct['id'] for ct in childTickets]

                # Add another level
                wbs.append(1)
                for c in childIDs:
                    wbs = _setLevel(c, wbs, level+1)
                # Remove the level we added
                wbs.pop()

            # Increment last element of wbs
            wbs[len(wbs)-1] += 1

            return wbs

        # Set WBS and level on all top level tickets (and recurse) If
        # a ticket's parent is not in the viewed tickets, consider it
        # top-level
        wbs = [ 1 ]
        roots = self.pm.roots(self.ticketsByID)
        for t in self.tickets:
            if t['id'] in roots:
                wbs = _setLevel(t['id'], wbs, 1)

    def _task_display(self, t, options):
        def _buildMap(field):
            self.classMap = {}
            i = 0
            for t in self.tickets:
                if t[field] not in self.classMap:
                    i = i + 1
                    self.classMap[t[field]] = i

        def _buildEnumMap(field):
            self.classMap = {}
            db = self.env.get_db_cnx()
            cursor = db.cursor()
            cursor.execute("SELECT name," +
                           db.cast('value', 'int') +
                           " FROM enum WHERE type=%s", (field,))
            for name, value in cursor:
                self.classMap[name] = value

        display = None
        colorBy = options['colorBy']

        # Build the map the first time we need it
        if self.classMap == None:
            # Enums (TODO: what others should I list?)
            if options['colorBy'] in ['priority', 'severity']:
                _buildEnumMap(colorBy)
            else:
                _buildMap(colorBy)

        # Set display based on class map
        if t[colorBy] in self.classMap:
            display = 'class=ticket-class%d' % self.classMap[t[colorBy]]

        # Add closed status for strike through
        if t['status'] == 'closed':
            if display == None:
                display = 'class=ticket-closed'
            else:
                display += ' ticket-closed'

        if display == None:
            display = '#ff7f3f'
        return display

    # Format a ticket into JavaScript source to display the
    # task. ticket is expected to have:
    #   children - child ticket IDs or None
    #   description - ticket description.
    #   id - ticket ID, an integer
    #   level - levels from root (0)
    #   link - What to link to
    #   owner - Used as resource name.
    #   percent - integer percent complete, 0..100 (or "act/est")
    #   priority - used to color the task
    #   calc_finish - end date (ignored if children is not None)
    #   self.fields[parent] - parent ticket ID
    #   self.fields[pred] - predecessor ticket IDs
    #   calc_start - start date (ignored if children is not None)
    #   status - string displayed in tool tip ; FIXME - not displayed yet
    #   summary - ticket summary
    #   type - string displayed in tool tip FIXME - not displayed yet
    def _format_ticket(self, ticket, options):
        # Translate owner to full name
        def _owner(ticket):
            if self.pm.isMilestone(ticket):
                owner_name = ''
            else:
                owner_name = ticket['owner']
                if options['userMap']:
                    # Build the map the first time we use it
                    if self.user_map is None:
                        self.user_map = {}
                        for username, name, email in self.env.get_known_users():
                            self.user_map[username] = name
                    # Map the user name
                    if self.user_map.get(owner_name):
                        owner_name = self.user_map[owner_name]
            return owner_name

        task = ''

        # pID, pName
        if self.pm.isMilestone(ticket):
            if ticket['id'] > 0:
                # Put ID number on inchpebbles
                name = 'MS:%s (#%s)' % (ticket['summary'], ticket['id'])
            else:
                # Don't show bogus ID of milestone pseudo tickets.
                name = 'MS:%s' % ticket['summary']
        else:
            name = "#%d:%s (%s %s)" % \
                   (ticket['id'], ticket['summary'],
                    ticket['status'], ticket['type'])
        task += 't = new JSGantt.TaskItem(%s,%d,"%s",' % \
            (self.GanttID, ticket['id'], javascript_quote(name))

        # pStart, pEnd
        task += '"%s",' % self.pm.start(ticket).strftime(self.pyDateFormat)
        task += '"%s",' % self.pm.finish(ticket).strftime(self.pyDateFormat)

        # pDisplay
        task += '"%s",' % javascript_quote(self._task_display(ticket, options))

        # pLink
        task += '"%s",' % javascript_quote(ticket['link'])

        # pMile
        if self.pm.isMilestone(ticket):
            task += '1,'
        else:
            task += '0,'

        # pRes (owner)
        task += '"%s",' % javascript_quote(_owner(ticket))

        # pComp (percent complete); integer 0..100
        task += '"%s",' % self.pm.percentComplete(ticket)

        # pGroup (has children)
        if self.pm.children(ticket):
            task += '%s,' % 1
        else:
            task += '%s,' % 0

        # pParent (parent task ID)
        # If there's no parent, don't link to it
        if self.pm.parent(ticket) == None:
            task += '%s,' % 0
        else:
            task += '%s,' % self.pm.parent(ticket)

        # open
        if int(ticket['level']) < int(options['openLevel']) and \
                ((options['expandClosedTickets'] != 0) or \
                     (ticket['status'] != 'closed')):
            openGroup = 1
        else:
            openGroup = 0
        task += '%d,' % openGroup

        # predecessors
        pred = [str(s) for s in self.pm.predecessors(ticket)]
        if len(pred):
            task += '"%s",' % javascript_quote(','.join(pred))
        else:
            task += '"%s",' % javascript_quote(','.join(''))

        # caption
        # FIXME - if caption isn't set to caption, use "" because the
        # description could be quite long and take a long time to make
        # safe and display.
        task += '"%s (%s %s)"' % (javascript_quote(ticket['description']),
                                  javascript_quote(ticket['status']),
                                  javascript_quote(ticket['type']))
        task += ');\n'
        task += self.GanttID+'.AddTaskItem(t);\n'
        return task

    def _filter_tickets(self, options, tickets):
        # Build the list of display filters from the configured value
        if not options.get('display') or options['display'] == '':
            displayFilter = {}
        else:
            # The general form is
            # 'display=field:value|field:value...'. Split on pipe to
            # get each part
            displayList = options['display'].split('|')

            # Process each part into the display filter
            displayFilter = {}
            field = None
            for f in displayList:
                parts = f.split(':')
                # Just one part, it's a value for the previous field
                if len(parts) == 1:
                    if field == None:
                        raise TracError(('display option error in "%s".' +
                                         ' Should be "display=f1:v1|f2:v2"' +
                                         ' or "display=f:v1|v2".') %
                                        options['display'])
                    else:
                        value = parts[0]
                else:
                    field = parts[0]
                    value = parts[1]

                if field in displayFilter:
                    displayFilter[field].append(value)
                else:
                    displayFilter[field] = [ value ]

        # If present and 1, true, otherwise false.
        if options.get('omitMilestones') \
                and int(options['omitMilestones']) == 1:
            omitMilestones = True
        else:
            omitMilestones = False

        # Filter the tickets
        filteredTickets = []
        for ticket in tickets:
            # Default to showing every ticket
            fieldDisplay = True

            if omitMilestones and \
                    self.pm.isTracMilestone(ticket):
                fieldDisplay = False
            else:
                # Process each element and disable display if all
                # filters fail to match. ((or) disjunction)
                for f in displayFilter:
                    display = True
                    for v in displayFilter[f]:
                        if ticket[f] == v:
                            display = True
                            break
                        display = False
                    fieldDisplay = fieldDisplay & display

            if fieldDisplay:

Answer: filteredTickets.append(ticket)

<think>
In the _filter_tickets method, after evaluating each ticket against the display filters, the code determines a boolean fieldDisplay. When fieldDisplay is true, the ticket should be kept in the result list. The typical action is to append the ticket to the filteredTickets list. Therefore the next line after the conditional is the append statement.
</think>
filteredTickets.append(ticket)


Context:
/* This file is part of VoltDB.
 * Copyright (C) 2008-2017 VoltDB Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package org.voltdb_testfuncs;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.voltdb.VoltType;
import org.voltdb.VoltTypeException;
import org.voltdb.types.GeographyPointValue;
import org.voltdb.types.GeographyValue;
import org.voltdb.types.TimestampType;

/** Contains a bunch of UDF's (user-defined functions) used for testing. */
public class UserDefinedTestFunctions {
    // Change this to true, to get a very altered version of most of these
    // UDF's (specifically, the ones with "add" or "concat" in their names);
    // useful for testing of @UpdateClasses (see ENG-12856)
    private boolean USE_ALTERNATIVE_VERSION = false;

    /** A simple user-defined (runtime) exception, used to test UDF's
     *  (user-defined exceptions) that throw such exceptions. */
    public class UserDefinedTestException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public UserDefinedTestException() {
        }
        public UserDefinedTestException(String message) {
            super(message);
        }
        public UserDefinedTestException(Throwable cause) {
            super(cause);
        }
        public UserDefinedTestException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    // We start with private methods that are called by (some of) the test
    // UDF's (user-defined functions) below:

    /**
     * Returns the VoltDB null value that corresponds to the data type
     * of the input value; for example, -128 for a TINYINT (byte) value,
     * or -32768 for a SMALLINT (short) value. However, if useTrueNullValue
     * is true, or if the value is not of a recognized numeric type, a
     * true Java null is returned.
     */
    private Number getNullValue(Number value, boolean useTrueNullValue) {
        if (useTrueNullValue) {
            return null;
        }

        switch (value.getClass().toString()) {
        case "class java.lang.Byte":        return VoltType.NULL_TINYINT;   // the TINYINT (byte) null value (-128)
        case "class java.lang.Short":       return VoltType.NULL_SMALLINT;  // the SMALLINT (short) null value (-32768)
        case "class java.lang.Integer":     return VoltType.NULL_INTEGER;   // the INTEGER (int) null value (-2147483648)
        case "class java.lang.Long":        return VoltType.NULL_BIGINT;    // the BIGINT (long) null value (-9223372036854775808L)
        case "class java.lang.Double":      return VoltType.NULL_FLOAT;     // the FLOAT (double) null value (-1.7E+308D)
        default:                            return null;
        }
    }

    public static class UDF_TEST {
        public static final int RETURN_DATA_TYPE_NULL = -100;
        public static final int RETURN_JAVA_NULL      = -101;
        public static final int RETURN_TINYINT_NULL   = -102;
        public static final int RETURN_SMALLINT_NULL  = -103;
        public static final int RETURN_INTEGER_NULL   = -104;
        public static final int RETURN_BIGINT_NULL    = -105;
        public static final int RETURN_FLOAT_NULL     = -106;
        public static final int RETURN_DECIMAL_MIN    = -107;
        public static final int RETURN_DECIMAL_MAX    = -108;
        public static final int RETURN_NaN            = -109;
        public static final int THROW_NullPointerException           = -110;
        public static final int THROW_IllegalArgumentException       = -111;
        public static final int THROW_NumberFormatException          = -112;
        public static final int THROW_ArrayIndexOutOfBoundsException = -113;
        public static final int THROW_ClassCastException             = -114;
        public static final int THROW_ArithmeticException            = -115;
        public static final int THROW_UnsupportedOperationException  = -116;
        public static final int THROW_VoltTypeException              = -117;
        public static final int THROW_UserDefinedTestException       = -118;
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. */
    private Number testExceptionsByValue(Number value) {
        return testExceptionsByValue(value, false);
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. When useTrueNullValue
     *  is true, and the special input value -100 is given, a true Java null
     *  is returned. */
    @SuppressWarnings("null")
    private Number testExceptionsByValue(Number value, boolean useTrueNullValue) {
        if (value == null) {
            return null;
        }
        Integer uninitialized = null;
        Integer[] array = {1, 2, 3, 4, 5};
        int intValue = value.intValue();

        switch (intValue) {
        case UDF_TEST.RETURN_DATA_TYPE_NULL:    return getNullValue(value, useTrueNullValue);
        case UDF_TEST.RETURN_JAVA_NULL:         return null;
        case UDF_TEST.RETURN_TINYINT_NULL:      return VoltType.NULL_TINYINT;   // the TINYINT (byte) null value (-128)
        case UDF_TEST.RETURN_SMALLINT_NULL:     return VoltType.NULL_SMALLINT;  // the SMALLINT (short) null value (-32768)
        case UDF_TEST.RETURN_INTEGER_NULL:      return VoltType.NULL_INTEGER;   // the INTEGER (int) null value (-2147483648)
        case UDF_TEST.RETURN_BIGINT_NULL:       return VoltType.NULL_BIGINT;    // the BIGINT (long) null value (-9223372036854775808L)
        case UDF_TEST.RETURN_FLOAT_NULL:        return VoltType.NULL_FLOAT;     // the FLOAT (double) null value (-1.7E+308D)
        case UDF_TEST.RETURN_DECIMAL_MIN:       return new BigDecimal("-99999999999999999999999999.999999999999");  // the DECIMAL (BigDecimal) minimum value
        case UDF_TEST.RETURN_DECIMAL_MAX:       return new BigDecimal( "99999999999999999999999999.999999999999");  // the DECIMAL (BigDecimal) maximum value
        case UDF_TEST.RETURN_NaN:               return Math.log(value.doubleValue());   // Return NaN
        case UDF_TEST.THROW_NullPointerException:           return uninitialized.intValue();        // Throw a NullPointerException (NPE)
        case UDF_TEST.THROW_IllegalArgumentException:       Character.toChars(intValue);            // Throw an IllegalArgumentException
        case UDF_TEST.THROW_NumberFormatException:          return new Byte("nonnumeric");          // Throw a NumberFormatException
        case UDF_TEST.THROW_ArrayIndexOutOfBoundsException: return array[-1];                       // Throw an ArrayIndexOutOfBoundsException
        case UDF_TEST.THROW_ClassCastException:             return (Byte) value + (Short) value;    // Throw a ClassCastException
        case UDF_TEST.THROW_ArithmeticException:            return intValue / 0;                    // Throw an ArithmeticException (/ by zero)
        case UDF_TEST.THROW_UnsupportedOperationException:  Arrays.asList(array).add(0);            // Throw an UnsupportedOperationException
        case UDF_TEST.THROW_VoltTypeException:              throw new VoltTypeException("Test UDF's that throw a VoltTypeException.");
        case UDF_TEST.THROW_UserDefinedTestException:       throw new UserDefinedTestException("Test UDF's that throw a user-defined (runtime) exception.");
        default:                                return value;
        }
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. When useTrueNullValue
     *  is true, and the special input value -100 is given, a true Java null
     *  is returned. */
    private Byte testExceptions(Byte value, boolean useTrueNullValue) {
        Number result = testExceptionsByValue(value, useTrueNullValue);
        return (result == null ? null : result.byteValue());
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. */
    private Byte testExceptions(Byte value) {
        return testExceptions(value, false);
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. */
    private Short testExceptions(Short value) {
        Number result = testExceptionsByValue(value);
        return (result == null ? null : result.shortValue());
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. When useTrueNullValue
     *  is true, and the special input value -100 is given, a true Java null
     *  is returned. */
    private Integer testExceptions(Integer value, boolean useTrueNullValue) {
        Number result = testExceptionsByValue(value, useTrueNullValue);
        return (result == null ? null : result.intValue());
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. */
    private Integer testExceptions(Integer value) {
        return testExceptions(value, false);
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. */
    private Long testExceptions(Long value) {
        Number result = testExceptionsByValue(value);
        return (result == null ? null : result.longValue());
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. When useTrueNullValue
     *  is true, and the special input value -100 is given, a true Java null
     *  is returned. */
    private Double testExceptions(Double value, boolean useTrueNullValue) {
        Number result = testExceptionsByValue(value, useTrueNullValue);
        return (result == null ? null : result.doubleValue());
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. */
    private Double testExceptions(Double value) {
        return testExceptions(value, false);
    }

    /** Usually just returns the input value; but certain special input values
     *  (generally between -100 and -120) trigger an exception to be thrown,
     *  or special VoltDB "null" values to be returned. */
    private BigDecimal testExceptions(BigDecimal value) {
        Number n = testExceptionsByValue(value);
        if (n == null) {
            return null;
        } else if (n.getClass() == BigDecimal.class) {
            return (BigDecimal) n;
        } else if (n.getClass() == Integer.class) {
            return new BigDecimal (n.intValue());
        } else if (n.getClass() == Long.class) {
            return new BigDecimal (n.longValue());
        } else {
            return new BigDecimal (n.doubleValue());
        }
    }

    /** Usually just returns the input value; but certain special input (int)
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be returned. */
    private String testExceptions(String value) {
        if (value == null) {
            return null;
        }
        try {
            Integer intValue = testExceptions(Integer.parseInt(value), true);
            return (intValue == null ? null : intValue.toString());
        } catch (NumberFormatException e) {
            return value;
        }
    }

    /** Usually just returns the input value; but certain special values within
     *  the input array (generally between -100 and -120) trigger an exception
     *  to be thrown, or special VoltDB "null" values to be returned. */
    private byte[] testExceptions(byte[] value) {
        if (value == null) {
            return null;
        }
        byte[] result = new byte[value.length];
        for (int i=0; i < value.length; i++) {
            Byte test_i = testExceptions(value[i], true);
            if (test_i == null) {
                return null;
            } else {
                result[i] = test_i;
            }
        }
        return result;
    }

    /** Usually just returns the input value; but certain special values within
     *  the input array (generally between -100 and -120) trigger an exception
     *  to be thrown, or special VoltDB "null" values to be returned. */
    // TODO: I'm not certain whether Byte[] (as opposed to byte[]) is a valid
    // way to represent VARBINARY
    private Byte[] testExceptions(Byte[] value) {
        if (value == null) {
            return null;
        }
        Byte[] result = new Byte[value.length];
        for (int i=0; i < value.length; i++) {
            Byte test_i = testExceptions(value[i], true);
            if (test_i == null) {
                return null;
            } else {
                result[i] = test_i;
            }
        }
        return result;
    }

    /** Usually just returns the input value; but input values with certain
     *  special years (generally between 1780 and 1800) trigger an exception
     *  to be thrown, or special VoltDB "null" values to be returned. */
    private TimestampType testExceptions(TimestampType value) {
        if (value == null) {
            return null;
        }
        Date date = value.asExactJavaDate();
        Integer year = testExceptions(date.getYear(), true);
        if (year == null) {
            return null;
        }
        date.setYear(year);
        return new TimestampType(date);
    }

    /** Usually just returns the input value; but input values with certain
     *  special years (generally between 1780 and 1800) trigger an exception
     *  to be thrown, or special VoltDB "null" values to be returned. */
    private TimestampType testExceptions(Date value) {
        if (value == null) {
            return null;
        }
        return testExceptions(new TimestampType(value));
    }

    /** Usually just returns the input value; but input values with certain
     *  special longitude values (generally between -100 and -120) trigger
     *  an exception to be thrown, or special VoltDB "null" values to be
     *  returned. */
    private GeographyPointValue testExceptions(GeographyPointValue value) {
        if (value == null) {
            return null;
        }
        // We don't bother to "test" Latitude, because it must be between -90
        // and 90, so it cannot have any of the "interesting" values, such as
        // -100, -101, etc.
        Double longitude = testExceptions(value.getLongitude(), true);
        return (longitude == null ? null : new GeographyPointValue(Math.min(180, Math.max(-180, longitude)), value.getLatitude() ) );
    }

    /** Usually just returns the input value; but input values with certain
     *  special longitude values (generally between -100 and -120) trigger
     *  an exception to be thrown, or special VoltDB "null" values to be
     *  returned. */
    private GeographyValue testExceptions(GeographyValue value) {
        if (value == null) {
            return null;
        }
        // We "test" every GeographyPoint in the Geography (polygon), but again,
        // we don't bother to "test" Latitude, because it must be between -90
        // and 90, so it cannot have any of the "interesting" values, such as
        // -100, -101, etc.
        List<List<GeographyPointValue>> rings = value.getRings();
        for (int i=0; i < rings.size(); i++) {
            List<GeographyPointValue> ring = rings.get(i);
            for (int j=0; j < ring.size(); j++) {
                if (testExceptions(ring.get(j).getLongitude(), true) == null) {
                    return null;
                }
            }
        }
        return value;
    }

    /** Private method called by the various test UDF's that involve addition
     *  of two integers; note that there is no null checking. However, if
     *  USE_ALTERNATIVE_VERSION is true, it subtracts, rather than adds, the
     *  two integers. */
    private Long add(Long i, Long j) {
        if (USE_ALTERNATIVE_VERSION) {
            return i - j;
        }
        return i + j;
    }

    // More private methods, using various integer data types, that simply call
    // the method above, with the necessary conversions:
    private Byte add(Byte i, Byte j) {
        return add(i.longValue(), j.longValue()).byteValue();
    }
    private Short add(Short i, Short j) {
        return add(i.longValue(), j.longValue()).shortValue();
    }
    private Integer add(Integer i, Integer j) {
        return add(i.longValue(), j.longValue()).intValue();
    }

    /** Private method called by the various test UDF's that involve addition
     *  of two (Double) numbers; note that there is no null checking. However,
     *  if USE_ALTERNATIVE_VERSION is true, it subtracts, rather than adds, the
     *  two integers. */
    private Double add(Double x, Double y) {
        if (USE_ALTERNATIVE_VERSION) {
            return x - y;
        }
        return x + y;
    }

    /** Private method called by the test UDF that involves addition of two
     *  (BigDecimal) numbers; note that there is no null checking. However,
     *  if USE_ALTERNATIVE_VERSION is true, it subtracts, rather than adds,
     *  the two integers. */
    private BigDecimal add(BigDecimal x, BigDecimal y) {
        if (USE_ALTERNATIVE_VERSION) {
            return x.subtract(y);
        }
        return x.add(y);
    }

    /** Private method called by the test UDF that involves "addition" of a
     *  GeographyPointValue to a GeographyValue; note that there is no null
     *  checking. However, if USE_ALTERNATIVE_VERSION is true, it "subtracts",
     *  rather than "adds", the GeographyPointValue. */
    private GeographyValue add(GeographyValue g, GeographyPointValue p) {
        if (USE_ALTERNATIVE_VERSION) {
            GeographyPointValue p2 = new GeographyPointValue(0.0 - p.getLongitude(),
                                                             0.0 - p.getLatitude());
            return g.add(p2);
        }
        return g.add(p);
    }

    /** Private method called by the various test UDF's that involve
     *  concatenation of two or more strings. However, if
     *  USE_ALTERNATIVE_VERSION is true, it concatenates them in
     *  reverse order. */
    private String concatenate(String... s) {
        if (s == null) {
            return null;
        }
        StringBuffer result = new StringBuffer();
        int len = s.length;
        for (int i=0; i < len; i++) {
            if (s[i] == null) {
                return null;
            }
            if (USE_ALTERNATIVE_VERSION) {
                result.append(s[len-1 - i]);
            } else {
                result.append(s[i]);
            }
        }
        return result.toString();
    }

    // Test UDF's (user-defined functions) that can be used to test UDF's that
    // throw various exceptions, or return various null values (including the
    // various numerical VoltDB null values, like -128 and -32768), when certain
    // special input values (generally between -100 and -120) are used; most
    // of these simply add or concatenate two input values:

    /** Simple test UDF (user-defined function) that adds two TINYINT
     *  (primitive, or unboxed, byte) values; except, certain special input
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be used. */
    public byte add2Tinyint(byte i, byte j) {
        if (i == VoltType.NULL_TINYINT || j == VoltType.NULL_TINYINT) {
            return VoltType.NULL_TINYINT;
        }
        return (byte) add(testExceptions(i), testExceptions(j));
    }

    /** Simple test UDF (user-defined function) that adds two TINYINT
     *  (boxed Byte) values; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public Byte add2TinyintBoxed(Byte i, Byte j) {
        Byte test_i = testExceptions(i);
        Byte test_j = testExceptions(j);
        if (test_i == null || test_j == null || test_i.equals(VoltType.NULL_TINYINT)
                || test_j.equals(VoltType.NULL_TINYINT)) {
            return null;
        }
        return (byte) add(test_i, test_j);
    }

    /** Simple test UDF (user-defined function) that adds two SMALLINT
     *  (primitive, or unboxed, short) values; except, certain special input
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be used. */
    public short add2Smallint(short i, short j) {
        if (i == VoltType.NULL_SMALLINT || j == VoltType.NULL_SMALLINT) {
            return VoltType.NULL_SMALLINT;
        }
        return (short) add(testExceptions(i), testExceptions(j));
    }

    /** Simple test UDF (user-defined function) that adds two SMALLINT
     *  (boxed Short) values; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public Short add2SmallintBoxed(Short i, Short j) {
        Short test_i = testExceptions(i);
        Short test_j = testExceptions(j);
        if (test_i == null || test_j == null || test_i.equals(VoltType.NULL_SMALLINT)
                || test_j.equals(VoltType.NULL_SMALLINT)) {
            return null;
        }
        return (short) add(test_i, test_j);
    }

    /** Simple test UDF (user-defined function) that adds two INTEGER
     *  (primitive, or unboxed, int) values; except, certain special input
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be used. */
    public int add2Integer(int i, int j) {
        if (i == VoltType.NULL_INTEGER || j == VoltType.NULL_INTEGER) {
            return VoltType.NULL_INTEGER;
        }
        return add(testExceptions(i), testExceptions(j));
    }

    /** Simple test UDF (user-defined function) that adds two INTEGER
     *  (boxed Integer) values; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public Integer add2IntegerBoxed(Integer i, Integer j) {
        Integer test_i = testExceptions(i);
        Integer test_j = testExceptions(j);
        if (test_i == null || test_j == null || test_i.equals(VoltType.NULL_INTEGER)
                || test_j.equals(VoltType.NULL_INTEGER)) {
            return null;
        }
        return add(test_i, test_j);
    }

    /** Simple test UDF (user-defined function) that adds two BIGINT
     *  (primitive, or unboxed, long) values; except, certain special input
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be used. */
    public long add2Bigint(long i, long j) {
        if (i == VoltType.NULL_BIGINT || j == VoltType.NULL_BIGINT) {
            return VoltType.NULL_BIGINT;
        }
        return add(testExceptions(i), testExceptions(j));
    }

    /** Simple test UDF (user-defined function) that adds two BIGINT
     *  (boxed Long) values; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public Long add2BigintBoxed(Long i, Long j) {
        Long test_i = testExceptions(i);
        Long test_j = testExceptions(j);
        if (test_i == null || test_j == null || test_i.equals(VoltType.NULL_BIGINT)
                || test_j.equals(VoltType.NULL_BIGINT)) {
            return null;
        }
        return add(test_i, test_j);
    }

    /** Simple test UDF (user-defined function) that adds two FLOAT
     *  (primitive, or unboxed, double) values; except, certain special input
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be used. */
    public double add2Float(double x, double y) {
        if (x <= VoltType.NULL_FLOAT || y <= VoltType.NULL_FLOAT) {
            return VoltType.NULL_FLOAT;
        }
        return add(testExceptions(x), testExceptions(y));
    }

    /** Simple test UDF (user-defined function) that adds two FLOAT
     *  (boxed Double) values; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public Double add2FloatBoxed(Double x, Double y) {
        Double test_x = testExceptions(x);
        Double test_y = testExceptions(y);
        if (test_x == null || test_y == null || test_x <= VoltType.NULL_FLOAT
                || test_y <= VoltType.NULL_FLOAT) {
            return null;
        }
        return add(test_x, test_y);
    }

    /** Simple test UDF (user-defined function) that adds two DECIMAL
     *  (BigDecimal) values; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public BigDecimal add2Decimal(BigDecimal x, BigDecimal y) {
        BigDecimal test_x = testExceptions(x);
        BigDecimal test_y = testExceptions(y);
        if (test_x == null || test_y == null) {
            return null;
        }
        return add(test_x, test_y);
    }

    /** Simple test UDF (user-defined function) that concatenates two VARCHAR
     *  (String) values; except, certain special input (int) values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public String add2Varchar(String s, String t) {
        return concatenate(testExceptions(s), testExceptions(t));
    }

    /** Simple test UDF (user-defined function) that "adds" two VARBINARY
     *  (primitive, or unboxed, byte array) values; that is, it adds each byte
     *  value in the two arrays; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public byte[] add2Varbinary(byte[] a, byte[] b) {
        byte[] test_a = testExceptions(a);
        byte[] test_b = testExceptions(b);
        if (test_a == null || test_b == null) {
            return null;
        }
        int length = Math.max(a.length, b.length);
        byte[] result = new byte[length];
        for (int i=0; i < length; i++) {
            result[i] = (byte) add( (i < a.length ? test_a[i] : 0),
                                    (i < b.length ? test_b[i] : 0) );
        }
        return result;
    }

    /** Simple test UDF (user-defined function) that adds two VARBINARY
     *  (boxed Byte array) values; that is, it adds each byte value in the
     *  two arrays; except, certain special input values (generally between
     *  -100 and -120) trigger an exception to be thrown, or special VoltDB
     *  "null" values to be used. */
    // TODO: I'm not certain whether Byte[] (as opposed to byte[]) is a valid
    // way to represent VARBINARY
    public Byte[] add2VarbinaryBoxed(Byte[] a, Byte[] b) {
        Byte[] test_a = testExceptions(a);
        Byte[] test_b = testExceptions(b);
        if (test_a == null || test_b == null) {
            return null;
        }
        int length = Math.max(a.length, b.length);
        Byte[] result = new Byte[length];
        for (int i=0; i < length; i++) {
            result[i] = (byte) add( (i < a.length ? test_a[i] : 0),
                                    (i < b.length ? test_b[i] : 0) );
        }
        return result;
    }

    /** Simple test UDF (user-defined function) that adds a specified number
     *  of years to a (VoltDB) Timestamp value; except, certain special input
     *  year values (generally between 1780 and 1800) trigger an exception to
     *  be thrown, or special VoltDB "null" values to be used. */
    public TimestampType addYearsToTimestamp(TimestampType t, Integer numYears) {
        TimestampType t2 = testExceptions(t);
        if (t2 == null || numYears == null) {
            return null;
        }
        Date d = t2.asExactJavaDate();
        d.setYear(add(d.getYear(), numYears));
        return new TimestampType(d);
    }

    /** A convenience method, allowing the above method to be called using a Date
     *  value, and it will construct a (VoltDB) Timestamp value to be used. */
    private TimestampType addYearsToTimestamp(Date d, int numYears) {
        return addYearsToTimestamp(new TimestampType(d), numYears);
    }

    /** Simple test UDF (user-defined function) that "adds" two (VoltDB)
     *  GeographyPoint values; that is, it adds their longitudes and latitudes;
     *  except, certain special longitude input values (generally between -100
     *  and -120) trigger an exception to be thrown, or special VoltDB "null"
     *  values to be used. */
    public GeographyPointValue add2GeographyPoint(GeographyPointValue p, GeographyPointValue q) {
        if (p == null || q == null) {
            return null;
        }
        // We don't bother to "test" Latitude, because it must be between -90
        // and 90, so it cannot have any of the "interesting" values, such as
        // -101, -102, etc.
        Double p_long = testExceptions(p.getLongitude(), true);
        Double q_long = testExceptions(q.getLongitude(), true);
        if (p_long == null || q_long == null) {
            return null;
        }
        return new GeographyPointValue( Math.min(180, Math.max(-180, add(p_long, q_long))), add(p.getLatitude(), q.getLatitude()) );
    }

    /** Simple test UDF (user-defined function) that "adds" a (VoltDB)
     *  GeographyPoint to a (VoltDB) Geography; that is, it adds the
     *  GeographyPoint's longitude and latitude to each vertex of the
     *  Geography; except, certain special longitude input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used. */
    public GeographyValue addGeographyPointToGeography(GeographyValue g, GeographyPointValue p) {
        GeographyValue g2 = testExceptions(g);
        if (g2 == null || p == null) {
            return null;
        }
        return add(g2, p);
    }

    // Test UDF's (user-defined functions) that are similar to (some of) the
    // above UDF's, but without null checking, so slightly odd things can happen,
    // such as null plus one equals a number

    /** Simple test UDF (user-defined function) that adds two TINYINT
     *  (primitive, or unboxed, byte) values; except, certain special input
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be used; but this version
     *  has no null checking. */
    public byte add2TinyintWithoutNullCheck(byte i, byte j) {
        return (byte) add(testExceptions(i), testExceptions(j));
    }
    /** Simple test UDF (user-defined function) that adds two TINYINT
     *  (boxed Byte) values; except, certain special input values (generally
     *  between -100 and -120) trigger an exception to be thrown, or special
     *  VoltDB "null" values to be used; but this version has no null
     *  checking. */
    public Byte add2TinyintBoxedWithoutNullCheck(Byte i, Byte j) {
        return (byte) add(testExceptions(i), testExceptions(j));
    }
    /** Simple test UDF (user-defined function) that adds two SMALLINT
     *  (primitive, or unboxed, short) values; except, certain special input
     *  values (generally between -100 and -120) trigger an exception to be
     *  thrown, or special VoltDB "null" values to be used; but this version
     *  has no null checking. */
    public short add2SmallintWithoutNullCheck(short i, short j) {

Answer: return (short) add(testExceptions(i), testExceptions(j));

<think>
The method add2SmallintWithoutNullCheck mirrors the earlier without‑null‑check methods. It should simply return the result of adding the two short inputs after passing them through testExceptions, casting the Long result to short. Hence the next line is the return statement with the appropriate cast.
</think>
return (short) add(testExceptions(i), testExceptions(j));


Context:
/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.pentaho.di.trans.steps.valuemapper;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.pentaho.di.core.KettleEnvironment;
import org.pentaho.di.core.RowMetaAndData;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettleValueException;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.core.plugins.StepPluginType;
import org.pentaho.di.core.row.RowMeta;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.row.value.ValueMetaString;
import org.pentaho.di.trans.RowProducer;
import org.pentaho.di.trans.RowStepCollector;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransHopMeta;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.step.StepInterface;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.di.trans.steps.dummytrans.DummyTransMeta;
import org.pentaho.di.trans.steps.injector.InjectorMeta;

import junit.framework.TestCase;

/**
 * Test class for the ValueMapper step.
 *
 * @author Sven Boden
 */
public class ValueMapperIT extends TestCase {
  public RowMetaInterface createRowMetaInterface() {
    RowMetaInterface rm = new RowMeta();

    ValueMetaInterface[] valuesMeta = { new ValueMetaString( "field1" ), };

    for ( int i = 0; i < valuesMeta.length; i++ ) {
      rm.addValueMeta( valuesMeta[i] );
    }

    return rm;
  }

  public List<RowMetaAndData> createData() {
    List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();

    RowMetaInterface rm = createRowMetaInterface();

    Object[] r1 = new Object[] { "abc" };
    Object[] r2 = new Object[] { "ABC" };
    Object[] r3 = new Object[] { "def" };
    Object[] r4 = new Object[] { "def" };
    Object[] r5 = new Object[] { "ghij" };
    Object[] r6 = new Object[] { "zzz" };
    Object[] r7 = new Object[] { "" };
    Object[] r8 = new Object[] { null };
    Object[] r9 = new Object[] { "abc" };

    list.add( new RowMetaAndData( rm, r1 ) );
    list.add( new RowMetaAndData( rm, r2 ) );
    list.add( new RowMetaAndData( rm, r3 ) );
    list.add( new RowMetaAndData( rm, r4 ) );
    list.add( new RowMetaAndData( rm, r5 ) );
    list.add( new RowMetaAndData( rm, r6 ) );
    list.add( new RowMetaAndData( rm, r7 ) );
    list.add( new RowMetaAndData( rm, r8 ) );
    list.add( new RowMetaAndData( rm, r9 ) );

    return list;
  }

  /**
   * Create the meta data for the results of test case 1.
   */
  public RowMetaInterface createRowMetaInterface1() {
    RowMetaInterface rm = new RowMeta();

    ValueMetaInterface[] valuesMeta =
    { new ValueMetaString( "field1" ), new ValueMetaString( "new_field" ), };

    for ( int i = 0; i < valuesMeta.length; i++ ) {
      rm.addValueMeta( valuesMeta[i] );
    }

    return rm;
  }

  /**
   * Create result data for test case 1.
   */
  public List<RowMetaAndData> createResultData1() {
    List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();

    RowMetaInterface rm = createRowMetaInterface1();

    Object[] r1 = new Object[] { "abc", "begin" };
    Object[] r2 = new Object[] { "ABC", "BEG_LONGER" };
    Object[] r3 = new Object[] { "def", "test" };
    Object[] r4 = new Object[] { "def", "test" };
    Object[] r5 = new Object[] { "ghij", null };
    Object[] r6 = new Object[] { "zzz", null };
    Object[] r7 = new Object[] { "", null };
    Object[] r8 = new Object[] { null, null };
    Object[] r9 = new Object[] { "abc", "begin" };

    list.add( new RowMetaAndData( rm, r1 ) );
    list.add( new RowMetaAndData( rm, r2 ) );
    list.add( new RowMetaAndData( rm, r3 ) );
    list.add( new RowMetaAndData( rm, r4 ) );
    list.add( new RowMetaAndData( rm, r5 ) );
    list.add( new RowMetaAndData( rm, r6 ) );
    list.add( new RowMetaAndData( rm, r7 ) );
    list.add( new RowMetaAndData( rm, r8 ) );
    list.add( new RowMetaAndData( rm, r9 ) );

    return list;
  }

  /**
   * Create result data for test case 2.
   */
  public List<RowMetaAndData> createResultData2() {
    List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();

    RowMetaInterface rm = createRowMetaInterface();

    Object[] r1 = new Object[] { "begin" };
    Object[] r2 = new Object[] { "BEG_LONGER" };
    Object[] r3 = new Object[] { "test" };
    Object[] r4 = new Object[] { "test" };
    Object[] r5 = new Object[] { null };
    Object[] r6 = new Object[] { "zzz" };
    Object[] r7 = new Object[] { null };
    Object[] r8 = new Object[] { null };
    Object[] r9 = new Object[] { "begin" };

    list.add( new RowMetaAndData( rm, r1 ) );
    list.add( new RowMetaAndData( rm, r2 ) );
    list.add( new RowMetaAndData( rm, r3 ) );
    list.add( new RowMetaAndData( rm, r4 ) );
    list.add( new RowMetaAndData( rm, r5 ) );
    list.add( new RowMetaAndData( rm, r6 ) );
    list.add( new RowMetaAndData( rm, r7 ) );
    list.add( new RowMetaAndData( rm, r8 ) );
    list.add( new RowMetaAndData( rm, r9 ) );

    return list;
  }

  /**
   * Create result data for test case 3.
   */
  public List<RowMetaAndData> createResultData3() {
    List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();

    RowMetaInterface rm = createRowMetaInterface();

    Object[] r1 = new Object[] { "begin" };
    Object[] r2 = new Object[] { "BEG_LONGER" };
    Object[] r3 = new Object[] { "test" };
    Object[] r4 = new Object[] { "test" };
    Object[] r5 = new Object[] { null };
    Object[] r6 = new Object[] { "zzz" };
    Object[] r7 = new Object[] { "emptyField" };
    Object[] r8 = new Object[] { "emptyField" };
    Object[] r9 = new Object[] { "begin" };

    list.add( new RowMetaAndData( rm, r1 ) );
    list.add( new RowMetaAndData( rm, r2 ) );
    list.add( new RowMetaAndData( rm, r3 ) );
    list.add( new RowMetaAndData( rm, r4 ) );
    list.add( new RowMetaAndData( rm, r5 ) );
    list.add( new RowMetaAndData( rm, r6 ) );
    list.add( new RowMetaAndData( rm, r7 ) );
    list.add( new RowMetaAndData( rm, r8 ) );
    list.add( new RowMetaAndData( rm, r9 ) );

    return list;
  }

  /**
   * Create result data for test case 4.
   */
  public List<RowMetaAndData> createResultData4() {
    List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();

    RowMetaInterface rm = createRowMetaInterface1();

    Object[] r1 = new Object[] { "abc", "begin" };
    Object[] r2 = new Object[] { "ABC", "BEG_LONGER" };
    Object[] r3 = new Object[] { "def", "test" };
    Object[] r4 = new Object[] { "def", "test" };
    Object[] r5 = new Object[] { "ghij", null };
    Object[] r6 = new Object[] { "zzz", "default" };
    Object[] r7 = new Object[] { "", null };
    Object[] r8 = new Object[] { null, null };
    Object[] r9 = new Object[] { "abc", "begin" };

    list.add( new RowMetaAndData( rm, r1 ) );
    list.add( new RowMetaAndData( rm, r2 ) );
    list.add( new RowMetaAndData( rm, r3 ) );
    list.add( new RowMetaAndData( rm, r4 ) );
    list.add( new RowMetaAndData( rm, r5 ) );
    list.add( new RowMetaAndData( rm, r6 ) );
    list.add( new RowMetaAndData( rm, r7 ) );
    list.add( new RowMetaAndData( rm, r8 ) );
    list.add( new RowMetaAndData( rm, r9 ) );

    return list;
  }

  /**
   * Create result data for test case 5.
   */
  public List<RowMetaAndData> createResultData5() {
    List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();

    RowMetaInterface rm = createRowMetaInterface();

    Object[] r1 = new Object[] { "begin" };
    Object[] r2 = new Object[] { "BEG_LONGER" };
    Object[] r3 = new Object[] { "test" };
    Object[] r4 = new Object[] { "test" };
    Object[] r5 = new Object[] { null };
    Object[] r6 = new Object[] { "default" };
    Object[] r7 = new Object[] { null };
    Object[] r8 = new Object[] { null };
    Object[] r9 = new Object[] { "begin" };

    list.add( new RowMetaAndData( rm, r1 ) );
    list.add( new RowMetaAndData( rm, r2 ) );
    list.add( new RowMetaAndData( rm, r3 ) );
    list.add( new RowMetaAndData( rm, r4 ) );
    list.add( new RowMetaAndData( rm, r5 ) );
    list.add( new RowMetaAndData( rm, r6 ) );
    list.add( new RowMetaAndData( rm, r7 ) );
    list.add( new RowMetaAndData( rm, r8 ) );
    list.add( new RowMetaAndData( rm, r9 ) );

    return list;
  }

  /**
   * Create result data for test case 6.
   */
  public List<RowMetaAndData> createResultData6() {
    List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();

    RowMetaInterface rm = createRowMetaInterface();

    Object[] r1 = new Object[] { "begin" };
    Object[] r2 = new Object[] { "BEG_LONGER" };
    Object[] r3 = new Object[] { "test" };
    Object[] r4 = new Object[] { "test" };
    Object[] r5 = new Object[] { "default" };
    Object[] r6 = new Object[] { "default" };
    Object[] r7 = new Object[] { "emptyField" };
    Object[] r8 = new Object[] { "emptyField" };
    Object[] r9 = new Object[] { "begin" };

    list.add( new RowMetaAndData( rm, r1 ) );
    list.add( new RowMetaAndData( rm, r2 ) );
    list.add( new RowMetaAndData( rm, r3 ) );
    list.add( new RowMetaAndData( rm, r4 ) );
    list.add( new RowMetaAndData( rm, r5 ) );
    list.add( new RowMetaAndData( rm, r6 ) );
    list.add( new RowMetaAndData( rm, r7 ) );
    list.add( new RowMetaAndData( rm, r8 ) );
    list.add( new RowMetaAndData( rm, r9 ) );

    return list;
  }

  /**
   * Check the 2 lists comparing the rows in order. If they are not the same fail the test.
   */
  public void checkRows( List<RowMetaAndData> rows1, List<RowMetaAndData> rows2 ) {
    int idx = 1;
    if ( rows1.size() != rows2.size() ) {
      fail( "Number of rows is not the same: " + rows1.size() + " and " + rows2.size() );
    }
    Iterator<RowMetaAndData> it1 = rows1.iterator();
    Iterator<RowMetaAndData> it2 = rows2.iterator();

    while ( it1.hasNext() && it2.hasNext() ) {
      RowMetaAndData rm1 = it1.next();
      RowMetaAndData rm2 = it2.next();

      Object[] r1 = rm1.getData();
      Object[] r2 = rm2.getData();

      if ( rm1.size() != rm2.size() ) {
        fail( "row nr " + idx + " is not equal" );
      }
      int[] fields = new int[r1.length];
      for ( int ydx = 0; ydx < r1.length; ydx++ ) {
        fields[ydx] = ydx;
      }
      try {
        if ( rm1.getRowMeta().compare( r1, r2, fields ) != 0 ) {
          fail( "row nr " + idx + " is not equal" );
        }
      } catch ( KettleValueException e ) {
        fail( "row nr " + idx + " is not equal" );
      }

      idx++;
    }
  }

  /**
   * Test case for valuemapper step. Injector step to a valuemapper step to a dummy step. Rows go in and should be
   * mapped accordingly.
   *
   * This test will write the mappings in a new field.
   */
  public void testValueMapper1() throws Exception {
    KettleEnvironment.init();

    //
    // Create a new transformation...
    //
    TransMeta transMeta = new TransMeta();
    transMeta.setName( "valuemappertest1" );

    PluginRegistry registry = PluginRegistry.getInstance();

    //
    // create an injector step...
    //
    String injectorStepname = "injector step";
    InjectorMeta im = new InjectorMeta();

    // Set the information of the injector.
    String injectorPid = registry.getPluginId( StepPluginType.class, im );
    StepMeta injectorStep = new StepMeta( injectorPid, injectorStepname, im );
    transMeta.addStep( injectorStep );

    //
    // Create a dummy step 1
    //
    String dummyStepname1 = "dummy step 1";
    DummyTransMeta dm1 = new DummyTransMeta();

    String dummyPid1 = registry.getPluginId( StepPluginType.class, dm1 );
    StepMeta dummyStep1 = new StepMeta( dummyPid1, dummyStepname1, dm1 );
    transMeta.addStep( dummyStep1 );

    TransHopMeta hi = new TransHopMeta( injectorStep, dummyStep1 );
    transMeta.addTransHop( hi );

    //
    // Create a ValueMapper step
    //
    String valueMapperName = "valuemapper step";
    ValueMapperMeta vm = new ValueMapperMeta();

    vm.setFieldToUse( "field1" );
    vm.setTargetField( "new_field" );
    vm.setSourceValue( new String[] { "abc", "ABC", "def", "ghij" } );
    vm.setTargetValue( new String[] { "begin", "BEG_LONGER", "test", "" } );

    String valueMapperPid = registry.getPluginId( StepPluginType.class, vm );
    StepMeta valueMapperStep = new StepMeta( valueMapperPid, valueMapperName, vm );
    transMeta.addStep( valueMapperStep );

    TransHopMeta hi2 = new TransHopMeta( dummyStep1, valueMapperStep );
    transMeta.addTransHop( hi2 );

    //
    // Create a dummy step 2
    //
    String dummyStepname2 = "dummy step 2";
    DummyTransMeta dm2 = new DummyTransMeta();

    String dummyPid2 = registry.getPluginId( StepPluginType.class, dm2 );
    StepMeta dummyStep2 = new StepMeta( dummyPid2, dummyStepname2, dm2 );
    transMeta.addStep( dummyStep2 );

    TransHopMeta hi3 = new TransHopMeta( valueMapperStep, dummyStep2 );
    transMeta.addTransHop( hi3 );

    // Now execute the transformation...
    Trans trans = new Trans( transMeta );

    trans.prepareExecution( null );

    StepInterface si = trans.getStepInterface( dummyStepname1, 0 );
    RowStepCollector dummyRc1 = new RowStepCollector();
    si.addRowListener( dummyRc1 );

    si = trans.getStepInterface( valueMapperName, 0 );
    RowStepCollector valueMapperRc = new RowStepCollector();
    si.addRowListener( valueMapperRc );

    si = trans.getStepInterface( dummyStepname2, 0 );
    RowStepCollector dummyRc = new RowStepCollector();
    si.addRowListener( dummyRc );

    RowProducer rp = trans.addRowProducer( injectorStepname, 0 );
    trans.startThreads();

    // add rows
    List<RowMetaAndData> inputList = createData();
    Iterator<RowMetaAndData> it = inputList.iterator();
    while ( it.hasNext() ) {
      RowMetaAndData rm = it.next();
      rp.putRow( rm.getRowMeta(), rm.getData() );
    }
    rp.finished();

    trans.waitUntilFinished();

    // Compare the results
    List<RowMetaAndData> resultRows = dummyRc.getRowsWritten();
    List<RowMetaAndData> goldenImageRows = createResultData1();

    checkRows( goldenImageRows, resultRows );
  }

  /**
   * Test case for valuemapper step. Injector step to a valuemapper step to a dummy step. Rows go in and should be
   * mapped accordingly.
   *
   * This test will write the mappings in the same field.
   */
  public void testValueMapper2() throws Exception {
    KettleEnvironment.init();

    //
    // Create a new transformation...
    //
    TransMeta transMeta = new TransMeta();
    transMeta.setName( "valuemappertest2" );

    PluginRegistry registry = PluginRegistry.getInstance();

    //
    // create an injector step...
    //
    String injectorStepname = "injector step";
    InjectorMeta im = new InjectorMeta();

    // Set the information of the injector.
    String injectorPid = registry.getPluginId( StepPluginType.class, im );
    StepMeta injectorStep = new StepMeta( injectorPid, injectorStepname, im );
    transMeta.addStep( injectorStep );

    //
    // Create a dummy step 1
    //
    String dummyStepname1 = "dummy step 1";
    DummyTransMeta dm1 = new DummyTransMeta();

    String dummyPid1 = registry.getPluginId( StepPluginType.class, dm1 );
    StepMeta dummyStep1 = new StepMeta( dummyPid1, dummyStepname1, dm1 );
    transMeta.addStep( dummyStep1 );

    TransHopMeta hi = new TransHopMeta( injectorStep, dummyStep1 );
    transMeta.addTransHop( hi );

    //
    // Create a step
    //
    String valueMapperName = "valuemapper step";
    ValueMapperMeta vm = new ValueMapperMeta();

    vm.setFieldToUse( "field1" );
    vm.setTargetField( "" );
    vm.setSourceValue( new String[] { "abc", "ABC", "def", "ghij" } );
    vm.setTargetValue( new String[] { "begin", "BEG_LONGER", "test", "" } );

    String valueMapperPid = registry.getPluginId( StepPluginType.class, vm );
    StepMeta valueMapperStep = new StepMeta( valueMapperPid, valueMapperName, vm );
    transMeta.addStep( valueMapperStep );

    TransHopMeta hi2 = new TransHopMeta( dummyStep1, valueMapperStep );
    transMeta.addTransHop( hi2 );

    //
    // Create a dummy step 2
    //
    String dummyStepname2 = "dummy step 2";
    DummyTransMeta dm2 = new DummyTransMeta();

    String dummyPid2 = registry.getPluginId( StepPluginType.class, dm2 );
    StepMeta dummyStep2 = new StepMeta( dummyPid2, dummyStepname2, dm2 );
    transMeta.addStep( dummyStep2 );

    TransHopMeta hi3 = new TransHopMeta( valueMapperStep, dummyStep2 );
    transMeta.addTransHop( hi3 );

    // Now execute the transformation...
    Trans trans = new Trans( transMeta );

    trans.prepareExecution( null );

    StepInterface si = trans.getStepInterface( dummyStepname1, 0 );
    RowStepCollector dummyRc1 = new RowStepCollector();
    si.addRowListener( dummyRc1 );

    si = trans.getStepInterface( valueMapperName, 0 );
    RowStepCollector valueMapperRc = new RowStepCollector();
    si.addRowListener( valueMapperRc );

    si = trans.getStepInterface( dummyStepname2, 0 );
    RowStepCollector dummyRc = new RowStepCollector();
    si.addRowListener( dummyRc );

    RowProducer rp = trans.addRowProducer( injectorStepname, 0 );
    trans.startThreads();

    // add rows
    List<RowMetaAndData> inputList = createData();
    Iterator<RowMetaAndData> it = inputList.iterator();
    while ( it.hasNext() ) {
      RowMetaAndData rm = it.next();
      rp.putRow( rm.getRowMeta(), rm.getData() );
    }
    rp.finished();

    trans.waitUntilFinished();

    // Compare the results
    List<RowMetaAndData> resultRows = dummyRc.getRowsWritten();
    List<RowMetaAndData> goldenImageRows = createResultData2();

    checkRows( goldenImageRows, resultRows );
  }

  /**
   * Test case for valuemapper step. Injector step to a valuemapper step to a dummy step. Rows go in and should be
   * mapped accordingly.
   *
   * This test will explicitly test the empty field processing.
   */
  public void testValueMapper3() throws Exception {
    KettleEnvironment.init();

    //
    // Create a new transformation...
    //
    TransMeta transMeta = new TransMeta();
    transMeta.setName( "valuemappertest3" );

    PluginRegistry registry = PluginRegistry.getInstance();

    //
    // create an injector step...
    //
    String injectorStepname = "injector step";
    InjectorMeta im = new InjectorMeta();

    // Set the information of the injector.
    String injectorPid = registry.getPluginId( StepPluginType.class, im );
    StepMeta injectorStep = new StepMeta( injectorPid, injectorStepname, im );
    transMeta.addStep( injectorStep );

    //
    // Create a dummy step 1
    //
    String dummyStepname1 = "dummy step 1";
    DummyTransMeta dm1 = new DummyTransMeta();

    String dummyPid1 = registry.getPluginId( StepPluginType.class, dm1 );
    StepMeta dummyStep1 = new StepMeta( dummyPid1, dummyStepname1, dm1 );
    transMeta.addStep( dummyStep1 );

    TransHopMeta hi = new TransHopMeta( injectorStep, dummyStep1 );
    transMeta.addTransHop( hi );

    //
    // Create a step
    //
    String valueMapperName = "valuemapper step";
    ValueMapperMeta vm = new ValueMapperMeta();

    vm.setFieldToUse( "field1" );
    vm.setTargetField( "" );
    vm.setSourceValue( new String[] { "abc", "ABC", "def", "ghij", null } );
    vm.setTargetValue( new String[] { "begin", "BEG_LONGER", "test", "", "emptyField" } );

    String valueMapperPid = registry.getPluginId( StepPluginType.class, vm );
    StepMeta valueMapperStep = new StepMeta( valueMapperPid, valueMapperName, vm );
    transMeta.addStep( valueMapperStep );

    TransHopMeta hi2 = new TransHopMeta( dummyStep1, valueMapperStep );
    transMeta.addTransHop( hi2 );

    //
    // Create a dummy step 2
    //
    String dummyStepname2 = "dummy step 2";
    DummyTransMeta dm2 = new DummyTransMeta();

    String dummyPid2 = registry.getPluginId( StepPluginType.class, dm2 );
    StepMeta dummyStep2 = new StepMeta( dummyPid2, dummyStepname2, dm2 );
    transMeta.addStep( dummyStep2 );

    TransHopMeta hi3 = new TransHopMeta( valueMapperStep, dummyStep2 );
    transMeta.addTransHop( hi3 );

    // Now execute the transformation...
    Trans trans = new Trans( transMeta );

    trans.prepareExecution( null );

    StepInterface si = trans.getStepInterface( dummyStepname1, 0 );
    RowStepCollector dummyRc1 = new RowStepCollector();
    si.addRowListener( dummyRc1 );

    si = trans.getStepInterface( valueMapperName, 0 );
    RowStepCollector valueMapperRc = new RowStepCollector();
    si.addRowListener( valueMapperRc );

    si = trans.getStepInterface( dummyStepname2, 0 );
    RowStepCollector dummyRc = new RowStepCollector();
    si.addRowListener( dummyRc );

    RowProducer rp = trans.addRowProducer( injectorStepname, 0 );
    trans.startThreads();

    // add rows
    List<RowMetaAndData> inputList = createData();
    Iterator<RowMetaAndData> it = inputList.iterator();
    while ( it.hasNext() ) {
      RowMetaAndData rm = it.next();
      rp.putRow( rm.getRowMeta(), rm.getData() );
    }
    rp.finished();

    trans.waitUntilFinished();

    // Compare the results
    List<RowMetaAndData> resultRows = dummyRc.getRowsWritten();
    List<RowMetaAndData> goldenImageRows = createResultData3();

    checkRows( goldenImageRows, resultRows );
  }

  /*----------------- TODO */
  /**
   * Test case for valuemapper step. Injector step to a valuemapper step to a dummy step. Rows go in and should be
   * mapped accordingly.
   *
   * This test will write the mappings in a new field, using a non matching default.
   */
  public void testValueMapper4() throws Exception {
    KettleEnvironment.init();

    //
    // Create a new transformation...
    //
    TransMeta transMeta = new TransMeta();
    transMeta.setName( "valuemappertest4" );

    PluginRegistry registry = PluginRegistry.getInstance();

    //
    // create an injector step...
    //
    String injectorStepname = "injector step";
    InjectorMeta im = new InjectorMeta();

    // Set the information of the injector.
    String injectorPid = registry.getPluginId( StepPluginType.class, im );
    StepMeta injectorStep = new StepMeta( injectorPid, injectorStepname, im );
    transMeta.addStep( injectorStep );

    //
    // Create a dummy step 1
    //
    String dummyStepname1 = "dummy step 1";
    DummyTransMeta dm1 = new DummyTransMeta();

    String dummyPid1 = registry.getPluginId( StepPluginType.class, dm1 );
    StepMeta dummyStep1 = new StepMeta( dummyPid1, dummyStepname1, dm1 );
    transMeta.addStep( dummyStep1 );

    TransHopMeta hi = new TransHopMeta( injectorStep, dummyStep1 );
    transMeta.addTransHop( hi );

    //
    // Create a step
    //
    String valueMapperName = "valuemapper step";
    ValueMapperMeta vm = new ValueMapperMeta();

    vm.setFieldToUse( "field1" );
    vm.setTargetField( "new_field" );
    vm.setNonMatchDefault( "default" );
    vm.setSourceValue( new String[] { "abc", "ABC", "def", "ghij" } );
    vm.setTargetValue( new String[] { "begin", "BEG_LONGER", "test", "" } );

    String valueMapperPid = registry.getPluginId( StepPluginType.class, vm );
    StepMeta valueMapperStep = new StepMeta( valueMapperPid, valueMapperName, vm );
    transMeta.addStep( valueMapperStep );

    TransHopMeta hi2 = new TransHopMeta( dummyStep1, valueMapperStep );
    transMeta.addTransHop( hi2 );

    //
    // Create a dummy step 2
    //
    String dummyStepname2 = "dummy step 2";
    DummyTransMeta dm2 = new DummyTransMeta();

    String dummyPid2 = registry.getPluginId( StepPluginType.class, dm2 );
    StepMeta dummyStep2 = new StepMeta( dummyPid2, dummyStepname2, dm2 );
    transMeta.addStep( dummyStep2 );

    TransHopMeta hi3 = new TransHopMeta( valueMapperStep, dummyStep2 );
    transMeta.addTransHop( hi3 );

    // Now execute the transformation...
    Trans trans = new Trans( transMeta );

    trans.prepareExecution( null );

    StepInterface si = trans.getStepInterface( dummyStepname1, 0 );
    RowStepCollector dummyRc1 = new RowStepCollector();
    si.addRowListener( dummyRc1 );

    si = trans.getStepInterface( valueMapperName, 0 );
    RowStepCollector valueMapperRc = new RowStepCollector();
    si.addRowListener( valueMapperRc );

    si = trans.getStepInterface( dummyStepname2, 0 );
    RowStepCollector dummyRc = new RowStepCollector();
    si.addRowListener( dummyRc );

    RowProducer rp = trans.addRowProducer( injectorStepname, 0 );
    trans.startThreads();

    // add rows
    List<RowMetaAndData> inputList = createData();
    Iterator<RowMetaAndData> it = inputList.iterator();
    while ( it.hasNext() ) {
      RowMetaAndData rm = it.next();
      rp.putRow( rm.getRowMeta(), rm.getData() );
    }
    rp.finished();

    trans.waitUntilFinished();

    // Compare the results
    List<RowMetaAndData> resultRows = dummyRc.getRowsWritten();
    List<RowMetaAndData> goldenImageRows = createResultData4();

    checkRows( goldenImageRows, resultRows );
  }

  /**
   * Test case for valuemapper step. Injector step to a valuemapper step to a dummy step. Rows go in and should be
   * mapped accordingly.
   *
   * This test will write the mappings in the same field. Using a non matching default.
   */
  public void testValueMapper5() throws Exception {
    KettleEnvironment.init();

    //
    // Create a new transformation...
    //
    TransMeta transMeta = new TransMeta();
    transMeta.setName( "valuemappertest5" );

    PluginRegistry registry = PluginRegistry.getInstance();

    //
    // create an injector step...
    //
    String injectorStepname = "injector step";
    InjectorMeta im = new InjectorMeta();

    // Set the information of the injector.
    String injectorPid = registry.getPluginId( StepPluginType.class, im );
    StepMeta injectorStep = new StepMeta( injectorPid, injectorStepname, im );
    transMeta.addStep( injectorStep );

    //
    // Create a dummy step 1
    //
    String dummyStepname1 = "dummy step 1";
    DummyTransMeta dm1 = new DummyTransMeta();

    String dummyPid1 = registry.getPluginId( StepPluginType.class, dm1 );
    StepMeta dummyStep1 = new StepMeta( dummyPid1, dummyStepname1, dm1 );
    transMeta.addStep( dummyStep1 );

    TransHopMeta hi = new TransHopMeta( injectorStep, dummyStep1 );
    transMeta.addTransHop( hi );

    //
    // Create a step
    //
    String valueMapperName = "valuemapper step";
    ValueMapperMeta vm = new ValueMapperMeta();

    vm.setFieldToUse( "field1" );

Answer: vm.setTargetField( "" );

<think>
In testValueMapper5, after configuring the ValueMapperMeta instance, the code follows the same pattern as earlier tests. The first configuration call is vm.setFieldToUse("field1");. Since this test writes mappings back to the same field, the next configuration should specify an empty target field, i.e., vm.setTargetField( "" );. This matches the ordering seen in similar test methods.
</think>
vm.setTargetField( "" );