From e0b0bece48a67440e5cd68bd28b4fad156376c92 Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 10 Dec 2024 10:53:02 -0500 Subject: [PATCH] i_843 Rebase changes --- .../pc2/clics/API202306/AccessService.java | 6 +- .../pc2/clics/API202306/CLICSContestInfo.java | 7 +- .../pc2/clics/API202306/CLICSEndpoint.java | 4 +- .../pc2/clics/API202306/CLICSProvider.java | 6 +- .../pc2/clics/API202306/CLICSScoreboard.java | 26 +- .../ecs/pc2/clics/API202306/CLICSTeam.java | 37 +- .../pc2/clics/API202306/CLICSVerionInfo.java | 6 +- .../clics/API202306/ClarificationService.java | 94 +-- .../pc2/clics/API202306/ContestService.java | 80 --- .../pc2/clics/API202306/ContestService.java~ | 603 ++++++++++++++++++ .../pc2/clics/API202306/EventFeedService.java | 16 +- .../ecs/pc2/clics/API202306/GroupService.java | 27 +- .../pc2/clics/API202306/JudgementService.java | 28 +- .../clics/API202306/JudgementTypeService.java | 17 +- .../pc2/clics/API202306/LanguageService.java | 30 +- .../clics/API202306/OrganizationService.java | 13 +- .../pc2/clics/API202306/ProblemService.java | 21 +- .../clics/API202306/ResourceConfig202306.java | 11 +- .../ecs/pc2/clics/API202306/RunService.java | 30 +- .../clics/API202306/ScoreboardService.java | 28 +- .../ecs/pc2/clics/API202306/StateService.java | 16 +- .../clics/API202306/SubmissionService.java | 34 +- .../ecs/pc2/clics/API202306/TeamService.java | 25 +- .../pc2/clics/API202306/VersionService.java | 4 +- src/edu/csus/ecs/pc2/ui/WebServerPane.java | 11 - 25 files changed, 804 insertions(+), 376 deletions(-) create mode 100644 src/edu/csus/ecs/pc2/clics/API202306/ContestService.java~ diff --git a/src/edu/csus/ecs/pc2/clics/API202306/AccessService.java b/src/edu/csus/ecs/pc2/clics/API202306/AccessService.java index b14eabac7..ea4153620 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/AccessService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/AccessService.java @@ -19,7 +19,7 @@ /** * WebService to handle "state" REST endpoint as described by the CLICS wiki. - * + * * @author John Buck * */ @@ -41,7 +41,7 @@ public AccessService(IInternalContest inModel, IInternalController inController) /** * This method returns a representation of the access that the connected user has - * + * * @return a {@link Response} object containing a JSON String giving the access information for the connected user */ @GET @@ -52,7 +52,7 @@ public Response getAccess(@Context SecurityContext sc, @PathParam("contestId") S if(contestId.equals(model.getContestIdentifier()) == true) { return Response.ok(new CLICSContestAccess(sc, model, controller, contestId).toJSON(), MediaType.APPLICATION_JSON).build(); } - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } @Override diff --git a/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java b/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java index 85b45672b..409bf2331 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.csus.ecs.pc2.core.Utilities; import edu.csus.ecs.pc2.core.model.ContestInformation; import edu.csus.ecs.pc2.core.model.ContestTime; @@ -16,7 +15,7 @@ /** * CLICS Contest Info. - * + * * @author John Buck * */ @@ -54,12 +53,12 @@ public class CLICSContestInfo { /** * Fill in properties for a contest. - * + * * @param model The contest to use */ public CLICSContestInfo(IInternalContest model) { ContestInformation ci = model.getContestInformation(); - + id = model.getContestIdentifier(); name = ci.getContestShortName(); formal_name = ci.getContestTitle(); diff --git a/src/edu/csus/ecs/pc2/clics/API202306/CLICSEndpoint.java b/src/edu/csus/ecs/pc2/clics/API202306/CLICSEndpoint.java index 8fe0b3c19..99ab4abfe 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/CLICSEndpoint.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/CLICSEndpoint.java @@ -9,7 +9,7 @@ /** * CLICS Endpoint * Contains information about an API endpoint that is supported - * + * * @author John Buck * */ @@ -24,7 +24,7 @@ public class CLICSEndpoint { /** * For use with the access endpoint. This describes the properties of a single endpoint. - * + * * @param type String representing the name of the endpoint, eg. "teams", "groups", etc. * @param properties List of supported properties */ diff --git a/src/edu/csus/ecs/pc2/clics/API202306/CLICSProvider.java b/src/edu/csus/ecs/pc2/clics/API202306/CLICSProvider.java index a987c64cd..d90a33eaf 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/CLICSProvider.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/CLICSProvider.java @@ -10,7 +10,7 @@ /** * CLICS Provider * Contains information about the system providing the API feed (PC2, in this case) - * + * * @author John Buck * */ @@ -28,12 +28,12 @@ public class CLICSProvider { /** * Fill in API Provider information properties (for the version endpoint) - * + * * @param versionInfo */ public CLICSProvider(VersionInfo versionInfo) { name = "pc2"; - version = versionInfo.getPC2Version() + " build " + versionInfo.getBuildNumber(); + version = versionInfo.getPC2Version() + " build " + versionInfo.getBuildNumber(); } public String toJSON() { diff --git a/src/edu/csus/ecs/pc2/clics/API202306/CLICSScoreboard.java b/src/edu/csus/ecs/pc2/clics/API202306/CLICSScoreboard.java index 85f35b48e..2d670f1a9 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/CLICSScoreboard.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/CLICSScoreboard.java @@ -31,7 +31,7 @@ /** * CLICS Scoreboard * Contains information about the scoreboard - * + * * @author John Buck * */ @@ -49,22 +49,28 @@ public class CLICSScoreboard { @JsonProperty private CLICSScoreboardRow [] rows; - + /** * Fill in the scoreboard information - * + * */ public CLICSScoreboard(IInternalContest model, IInternalController controller, Group group, Integer division) throws IllegalContestState, JAXBException, IOException { - + DefaultScoringAlgorithm scoringAlgorithm = new DefaultScoringAlgorithm(); Properties properties = ScoreboardUtilities.getScoringProperties(model); - // legacy - standings are created as XML, and we convert that to JSON. - String xml = scoringAlgorithm.getStandings(model, null, division, group, properties, StaticLog.getLog()); - + ArrayList groupList = null; + + if(group != null) { + groupList = new ArrayList(); + groupList.add(group); + } + // legacy - standings are created as XML, and we convert that to JSON. + String xml = scoringAlgorithm.getStandings(model, null, division, groupList, properties, StaticLog.getLog()); + ContestStandings contestStandings = ScoreboardUtilities.createContestStandings(xml); - + // This is what we want to return: // { // "time": "2014-06-25T14:13:07.832+01", @@ -90,10 +96,10 @@ public CLICSScoreboard(IInternalContest model, IInternalController controller, G time = ZonedDateTime.now( ZoneOffset.UTC ).format( DateTimeFormatter.ISO_INSTANT); contest_time = model.getContestTime().getElapsedTimeStr(); state = new CLICSContestState(model); - + ArrayListrowsArray = new ArrayList(); HashMap probEleToShort = new HashMap(); - + // create a mapping of each problem's element ID to its shortname. // we will use shortname as the problem id in the problem list for each team's solutions for(Problem problem: model.getProblems()) { diff --git a/src/edu/csus/ecs/pc2/clics/API202306/CLICSTeam.java b/src/edu/csus/ecs/pc2/clics/API202306/CLICSTeam.java index ffe2c4025..ba4b18318 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/CLICSTeam.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/CLICSTeam.java @@ -4,6 +4,7 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -17,6 +18,7 @@ import edu.csus.ecs.pc2.core.model.ClientId; import edu.csus.ecs.pc2.core.model.ClientType; import edu.csus.ecs.pc2.core.model.ClientType.Type; +import edu.csus.ecs.pc2.core.model.ElementId; import edu.csus.ecs.pc2.core.model.Group; import edu.csus.ecs.pc2.core.model.IInternalContest; import edu.csus.ecs.pc2.core.security.Permission; @@ -85,10 +87,14 @@ public CLICSTeam(IInternalContest model, Account account) { if (JSONTool.notEmpty(account.getInstitutionCode()) && !account.getInstitutionCode().equals("undefined")) { organization_id = JSONTool.getOrganizationId(account); } - if (account.getGroupId() != null) { - group_ids = new String[1]; - // FIXME eventually accounts should have more then 1 groupId, make sure add them - group_ids[0] = JSONTool.getGroupId(model.getGroup(account.getGroupId())); + if (account.getGroupIds() != null) { + HashSet groups = account.getGroupIds(); + ArrayList groupList = new ArrayList(); + + for(ElementId ele : groups) { + groupList.add(JSONTool.getGroupId(model.getGroup(ele))); + } + group_ids = (String [])groupList.toArray(); } hidden = !account.isAllowed(Permission.Type.DISPLAY_ON_SCOREBOARD); } @@ -197,16 +203,21 @@ public String toJSON() { } // now fill in any other fields we can if(team.group_ids != null && team.group_ids.length > 0) { - Group group = jsontool.getGroupFromNumber(team.group_ids[0]); - if(group == null) { - log.log(Log.SEVERE, "No group has been defined with GroupId=" + team.group_ids[0]); - error = true; - break; + Group group; + boolean bFirst = true; + // team has a list of group names + for(String groupName : team.group_ids) { + group = jsontool.getGroupFromNumber(groupName); + if(group == null) { + log.log(Log.SEVERE, "No group has been defined with GroupId=" + groupName); + error = true; + break; + } + account.addGroupId(group.getElementId(), bFirst); + bFirst = false; } - account.setGroupId(group.getElementId()); - //TODO fix this when PC2 supports multiple groups per account - if(team.group_ids.length > 1) { - log.log(Log.INFO, account.getDisplayName() + " has " + team.group_ids.length + " groups assigned - only using first one"); + if(error) { + break; } } if(team.hidden) { diff --git a/src/edu/csus/ecs/pc2/clics/API202306/CLICSVerionInfo.java b/src/edu/csus/ecs/pc2/clics/API202306/CLICSVerionInfo.java index 8fbc55bdf..ae61eae61 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/CLICSVerionInfo.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/CLICSVerionInfo.java @@ -9,7 +9,7 @@ /** * CLICS Version Info. - * + * * @author Douglas A. Lane * @author John Buck * @@ -24,10 +24,10 @@ public class CLICSVerionInfo { @JsonProperty private CLICSProvider provider; - + /** * Fill in the API version information properties - * + * * @param versionInfo */ public CLICSVerionInfo(VersionInfo versionInfo) { diff --git a/src/edu/csus/ecs/pc2/clics/API202306/ClarificationService.java b/src/edu/csus/ecs/pc2/clics/API202306/ClarificationService.java index 7a93673f2..29074d682 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/ClarificationService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/ClarificationService.java @@ -21,8 +21,8 @@ import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.SecurityContext; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.JsonMappingException; @@ -42,9 +42,9 @@ import edu.csus.ecs.pc2.services.eventFeed.WebServer; /** - * WebService to handle languages - * - * @author ICPC + * WebService to handle clarifications + * + * @author John Buck * */ @Path("/contests/{contestId}/clarifications") @@ -52,7 +52,7 @@ @Provider @Singleton public class ClarificationService implements Feature { - + private IInternalContest model; private IInternalController controller; @@ -65,7 +65,7 @@ public ClarificationService(IInternalContest inContest, IInternalController inCo /** * This method returns a representation of the current contest clarifications in JSON format. The returned value is a JSON array with one clarification description per array element, complying with 2023-06 - * + * * @param sc security info for the user making the request * @param contestId Contest for which info is requested * @return a {@link Response} object containing the contest languages in JSON form @@ -76,14 +76,14 @@ public Response getClarifications(@Context SecurityContext sc, @PathParam("conte // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } - + // get the groups from the contest Clarification[] clarifications = model.getClarifications(); - + ArrayList clarList = new ArrayList(); - + // these are the only 2 that have special rules. boolean isStaff = sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) || sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE); boolean isTeam = sc.isUserInRole(WebServer.WEBAPI_ROLE_TEAM); @@ -108,7 +108,7 @@ public Response getClarifications(@Context SecurityContext sc, @PathParam("conte /** * This method returns a representation of the current contest clarification requested in JSON format. The returned value is a single clarification in json, Complying with 2023-06 - * + * * @param sc security info for the user making the request * @param contestId Contest for which info is requested * @param clarificationId the id of the desired clarification @@ -121,11 +121,11 @@ public Response getClarification(@Context SecurityContext sc, @PathParam("contes // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } // get the groups from the contest Clarification[] clarifications = model.getClarifications(); - + // these are the only 2 that have special rules. boolean isStaff = sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) || sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE); boolean isTeam = sc.isUserInRole(WebServer.WEBAPI_ROLE_TEAM); @@ -133,11 +133,11 @@ public Response getClarification(@Context SecurityContext sc, @PathParam("contes String user = sc.getUserPrincipal().getName(); ClarificationAnswer[] clarAnswers = null; - + // keep track of whether the clarificationId specified was for the question, in which case this will // be set to non-null Clarification clarNoAnswer = null; - + // create list of clarifications to send back for (Clarification clarification: clarifications) { if (clarification.isSendToAll() || isStaff || (isTeam && isClarificationForUser(clarification, user))) { @@ -168,14 +168,14 @@ public Response getClarification(@Context SecurityContext sc, @PathParam("contes return Response.ok(json, MediaType.APPLICATION_JSON).build(); } catch (Exception e) { return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error creating JSON for clarification " + clarificationId + " " + e.getMessage()).build(); - } + } } return Response.status(Response.Status.NOT_FOUND).build(); } /** * Post a new clarification - * + * * @param servletRequest details of request * @param sc requesting user's authorization info * @param contestId The contest @@ -189,14 +189,14 @@ public Response addNewClarification(@Context HttpServletRequest servletRequest, // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } - + // only admin, hydge, or team can create a clarification. And team can do it only if contest is started. if(!sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) && !sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE) && (!sc.isUserInRole(WebServer.WEBAPI_ROLE_TEAM) || !model.getContestTime().isContestStarted())) { return Response.status(Response.Status.FORBIDDEN).build(); } - + // check for empty request if (jsonInputString == null || jsonInputString.length() == 0) { // return HTTP 400 response code per CLICS spec @@ -215,13 +215,13 @@ public Response addNewClarification(@Context HttpServletRequest servletRequest, if(StringUtilities.isEmpty(clar.getText())) { return Response.status(Status.BAD_REQUEST).entity("text must not be empty").build(); } - + if(!sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) && (clar.getTo_team_id() != null || clar.getTime() != null || clar.getContest_time() != null)) { return Response.status(Status.BAD_REQUEST).entity("may not include one or more properties").build(); } - + String user = sc.getUserPrincipal().getName(); - + // if team specifies "from id", it must be that of the current user. if(!StringUtilities.isEmpty(clar.getFrom_team_id())) { String fullUser = "team" + clar.getFrom_team_id(); @@ -243,7 +243,7 @@ public Response addNewClarification(@Context HttpServletRequest servletRequest, } catch (Exception e) { return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error creating JSON for clarification " + e.getMessage()).build(); } - + } else { controller.getLog().log(Log.WARNING, "Can not find problem for id " + clar.getProblem_id()); } @@ -252,10 +252,10 @@ public Response addNewClarification(@Context HttpServletRequest servletRequest, } return Response.status(Response.Status.NOT_FOUND).build(); } - + /** * Put updates an existing clarification (presumably an answer) - * + * * @param servletRequest details of request * @param sc requesting user's authorization info * @param contestId The contest @@ -266,17 +266,17 @@ public Response addNewClarification(@Context HttpServletRequest servletRequest, @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response updateClarification(@Context HttpServletRequest servletRequest, @Context SecurityContext sc, @PathParam("contestId") String contestId, String jsonInputString) { - + // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } - + // only admin or judge can update a clarification. if(!sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) && !sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE)) { return Response.status(Response.Status.FORBIDDEN).build(); } - + // check for empty request if (jsonInputString == null || jsonInputString.length() == 0) { // return HTTP 400 response code per CLICS spec @@ -288,18 +288,18 @@ public Response updateClarification(@Context HttpServletRequest servletRequest, // return HTTP 400 response code per CLICS spec return Response.status(Status.BAD_REQUEST).entity("invalid json supplied").build(); } - + if(StringUtilities.isEmpty(clar.getText())) { return Response.status(Status.BAD_REQUEST).entity("text must not be empty").build(); } - - + + return Response.status(Response.Status.NOT_IMPLEMENTED).build(); } /** * Check if the supplied clarification is from/to the supplied user - * + * * @param clarification the clarification to check * @param user the user to check * @return true if the user is allowed to see this clarification @@ -307,21 +307,21 @@ public Response updateClarification(@Context HttpServletRequest servletRequest, private boolean isClarificationForUser(Clarification clarification, String user) { return(clarification.getSubmitter().getName().equals(user)); } - + /** * Tests if the supplied user context has a role to submit clarifications as a team - * + * * @param sc User's security context * @return true of the user can submit clarifications */ public static boolean isTeamSubmitClarificationAllowed(SecurityContext sc) { return(sc.isUserInRole(WebServer.WEBAPI_ROLE_TEAM)); } - + /** * Tests if the supplied user context has a role to submit clarifications - * + * * @param sc User's security context * @return true of the user can submit clarifications */ @@ -331,17 +331,17 @@ public static boolean isAdminSubmitClarificationAllowed(SecurityContext sc) { /** * Tests if the supplied user context has a role to submit clarifications on behalf of a team - * + * * @param sc User's security context * @return true of the user can submit clarifications on behalf of a team */ public static boolean isProxySubmitClarificationAllowed(SecurityContext sc) { return(sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN)); } - + /** * Retrieve access information about this endpoint for the supplied user's security context - * + * * @param sc User's security information * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise */ @@ -351,7 +351,7 @@ public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { /** * Converts the input string, assumed to be a JSON string, into a {@link Map} of JSON key-value pairs. - * + * * @param contestId contest identifier * @param jsonRequestString * a JSON string specifying a starttime request in CLICS format @@ -377,7 +377,7 @@ private Map parseJSONIntoMap(String contestId, String jsonReques return jsonDataMap; } - + /** * Returns a ClientId based on the user supplied. eg. "team99", "administrator1", etc. * @param user eg. team99 @@ -385,7 +385,7 @@ private Map parseJSONIntoMap(String contestId, String jsonReques */ private ClientId getClientIdFromUser(String user) { ClientId clientId = null; - + // basically, need to match lower case letters followed by digits Matcher matcher = Pattern.compile("^([a-z]+)([0-9]+)$").matcher(user); if(matcher.matches()) { @@ -397,10 +397,10 @@ private ClientId getClientIdFromUser(String user) { } return clientId; } - + /** * Returns the the Problem object for supplied id (short name) or null if none found - * + * * @param id shortname of problem * @return Problem object or null */ @@ -412,7 +412,7 @@ private Problem getProblemFromId(String id) { } return(null); } - + @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java b/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java index c79def784..7597752af 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java @@ -1,5 +1,4 @@ // Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. - package edu.csus.ecs.pc2.clics.API202306; import java.io.IOException; @@ -245,28 +244,6 @@ public Response setContestTimes(@Context HttpServletRequest servletRequest, @Con */ private Response HandleContestStartTime(SecurityContext sc, String contestId, String startTimeValueString, boolean sawCountdownPauseTime) { - // check for count down pause time - if(countdownPauseTime != null) { - // can't have both a start time and count down pause time - if(startTimeValueString != null) { - controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": only one of '" + CONTEST_START_TIME_KEY + "' key or '" + CONTEST_COUNTDOWN_PAUSE_TIME_KEY + "' may be specified"); - // return HTTP 400 response - return Response.status(Status.BAD_REQUEST).entity("Only one of '" + CONTEST_START_TIME_KEY + "' key or '" + CONTEST_COUNTDOWN_PAUSE_TIME_KEY + "' may be specified for " + contestsEndpoint).build(); - } - return(HandleContestCountdownPauseTime(contestId, countdownPauseTime)); - } - return(HandleContestStartTime(sc, contestId, startTimeValueString, sawCountdownPauseTime)); - } - - /** - * Process contest start time - * - * @param contestId which contest - * @param startTimeValueString new contest start time (ISO format) or null to make it undefined - * @return web response - */ - private Response HandleContestStartTime(String contestId, String startTimeValueString) { - StartTimeRequestType requestType = StartTimeRequestType.ILLEGAL; GregorianCalendar requestedStartTime = null; String logString = LOG_PREFIX + contestId + ": received '" + CONTEST_START_TIME_KEY + "': "; @@ -384,12 +361,6 @@ private Response HandleContestStartTime(String contestId, String startTimeValueS // break; - case SET_COUNTDOWN_PAUSE_TIME: - // TODO: tell PC2 to stop countdown when clock is 'pauseTime' ms away from start - controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": countdown_pause_time not implemented"); - return Response.status(Status.NOT_MODIFIED).entity("Unable to set countdown_pause_time to " + pauseTime + " ms").build(); - //break; - default: // shouldn't be able to get here! controller.getLog().log(Log.SEVERE, LOG_PREFIX + contestId + ": setStarttime(): unknown default condition: request type = " + requestType); @@ -458,57 +429,6 @@ private Response HandleContestThawTime(SecurityContext sc, String contestId, Str } - /** - * Process Contest count down pause - * - * @param contestId which contest - * @param countdownPauseTime how long before contest start should the count down pause (CLICS RELTIME value) - * @return web resposne - */ - private Response HandleContestCountdownPauseTime(String contestId, String countdownPauseTime) { - - controller.getLog().log(Log.DEBUG, LOG_PREFIX + contestId + ": received '" + CONTEST_COUNTDOWN_PAUSE_TIME_KEY + "': " + countdownPauseTime); - - long pauseTime = Utilities.convertCLICSContestTimeToMS(countdownPauseTime); - - // MIN_VALUE is returned on format error - if(pauseTime != Long.MIN_VALUE) { - // want to stop the countdown with this many milliseconds left - // TODO: tell PC2 to stop countdown when clock is 'pauseTime' ms away from start - controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": countdown_pause_time not implemented"); - return Response.status(Status.NOT_MODIFIED).entity("Unable to set countdown_pause_time to " + pauseTime + " ms").build(); - } - return Response.status(Status.BAD_REQUEST).entity("Bad value for count down pause time request").build(); - } - - /** - * Process contest thaw time and generate response - * - * @param contestId which contest - * @param thawTimeValue ISO date of when the contest should unfreeze - * @return web response - */ - private Response HandleContestThawTime(SecurityContext sc, String contestId, String thawTimeValue) { - - // check authorization (verify requester is allowed to make this request) - if (!isContestThawAllowed(sc)) { - controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": unauthorized request"); - // return HTTP 401 response code per CLICS spec - return Response.status(Status.UNAUTHORIZED).entity("You are not authorized to access this page").build(); - } - - // thaw time present, validate now - GregorianCalendar thawTime = getDate(contestId, thawTimeValue); - if (thawTime != null) { - // Set thaw time to this time. - // TODO: tell PC2 to thaw the contest at the given time. - controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": setting of contest thaw time is not implemented"); - return Response.status(Status.NOT_MODIFIED).entity("Unable to set contest thaw time to " + thawTime.toString()).build(); - } - return Response.status(Status.BAD_REQUEST).entity("Bad value for contest thaw time request").build(); - - } - /** * Parses the given String and returns a {@link GregorianCalendar} object if the String represents a valid Unix Epoch date; otherwise returns null. * diff --git a/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java~ b/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java~ new file mode 100644 index 000000000..40a289593 --- /dev/null +++ b/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java~ @@ -0,0 +1,603 @@ +// Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +package edu.csus.ecs.pc2.clics.API202306; + +import java.io.IOException; +import java.text.ParseException; +import java.util.GregorianCalendar; +import java.util.Map; + +import javax.inject.Singleton; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PATCH; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.ext.Provider; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.MapType; + +import edu.csus.ecs.pc2.core.IInternalController; +import edu.csus.ecs.pc2.core.Utilities; +import edu.csus.ecs.pc2.core.log.Log; +import edu.csus.ecs.pc2.core.model.ContestInformation; +import edu.csus.ecs.pc2.core.model.IInternalContest; +import edu.csus.ecs.pc2.services.core.JSONUtilities; +import edu.csus.ecs.pc2.services.eventFeed.WebServer; + +/** + * WebService to handle "contests/" and "contests/" REST endpoints as described by the CLICS wiki. + * + * example get output: + * { + * "id": "wf2014", + * "name": "2014 ICPC World Finals", + * "formal_name": "38th Annual World Finals of the ACM International Collegiate Programming Contest", + * "start_time": "2014-06-25T10:00:00+01", + * "duration": "5:00:00", + * "scoreboard_freeze_duration": "1:00:00", + * "scoreboard_type": "pass-fail", + * "penalty_time": 20 + * } + * + * example patch request (to set contest start time): + * { + * "id":"7b0dd4ea-19a1-4434-9034-529ebe55ab45", + * "start_time":"2014-06-25T10:00:00+01" + * } + * + * or, to pause the countdown: + * { + * "id":"wf2016", + * "start_time":null + * } + * + * or, to change thaw time: + * { + * "id": "wf2014", + * "scoreboard_thaw_time": "2014-06-25T19:30:00+01" + * } + * + * @author pc2@ecs.csus.edu + * + */ +@Path("/contests") +@Produces(MediaType.APPLICATION_JSON) +@Provider +@Singleton +public class ContestService implements Feature { + + private static final String LOG_PREFIX = "Contest Service " + ResourceConfig202306.CLICS_API_VERSION + ": PATCH for "; + + private static final String CONTEST_ID_KEY = "id"; + private static final String CONTEST_START_TIME_KEY = "start_time"; + private static final String CONTEST_COUNTDOWN_PAUSE_TIME_KEY = "countdown_pause_time"; + private static final String CONTEST_THAW_TIME = "scoreboard_thaw_time"; + + private static final long MIN_MS_TO_START_OF_CONTEST = 30 * 1000; + + private IInternalContest model; + + private IInternalController controller; + + /** + * List of the possible types of requests which might be received from clients. + * + * @author john + */ + private enum StartTimeRequestType { + ILLEGAL, SET_START_TO_UNDEFINED, SET_START_TO_SPECIFIED_DATE, SET_COUNTDOWN_PAUSE_TIME, SET_CONTEST_THAW_TIME + }; + + public ContestService(IInternalContest inModel, IInternalController inController) { + super(); + this.model = inModel; + this.controller = inController; + } + + /** + * This method resets the current contest scheduled start time according to the received (input) string, which it expects to be in JSON format as described in the CLICS Wiki "StartTime" interface + * specification. + * + * @return a {@link Response} object indicating the status of the setStarttime request as follows (from the CLI Wiki Contest_Start_Interface spec): + * + *
+     *         // PUT HTTP body is application/json:
+     *         // { "starttime":1265335138.26 }
+     * // or:
+     * // { "starttime":"undefined" }
+     * // HTTP response is:
+     * // 200: if successful.
+     * // 400: if the payload is invalid json, start time is invalid, etc.
+     * // 401: if authentication failed.
+     * // 403: if contest is already started
+     * // 403: if setting to 'null' with less than 30s left to previous start time.
+     * // 403: if setting to new (defined) start time with less than 30s left to previous start time.
+     * // 403: if the new start time is less than 30s from now.
+     *         
+ * + * @param servletRequest + * @param sc + * @param contestId + * @param jsonInputString + * @return + */ + @PATCH + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + @Path("{contestId}/") + public Response setContestTimes(@Context HttpServletRequest servletRequest, @Context SecurityContext sc, @PathParam("contestId") String contestId, String jsonInputString) { + + // shorthand since we use this a bit + String contestsEndpoint = "/contests/" + contestId + " PATCH request"; + + controller.getLog().log(Log.DEBUG, LOG_PREFIX + contestId + " received the following request body: " + jsonInputString); + + // check for empty request + if (jsonInputString == null || jsonInputString.length() == 0) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": received invalid (empty) JSON string"); + // return HTTP 400 response code per CLICS spec + return Response.status(Status.BAD_REQUEST).entity("Empty contest request").build(); + } + + // we got some potentially legal input; try parsing it for valid form + Map requestMap = parseJSONIntoMap(contestId, jsonInputString); + + // if the map is null then the parsing failed + if (requestMap == null) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": unable to parse JSON starttime string"); + // return HTTP 400 response code per CLICS spec + return Response.status(Status.BAD_REQUEST).entity("Bad JSON starttime request").build(); + } + + // if we get here then the JSON parsed correctly; see if it contained "starttime" as a key + if (!requestMap.containsKey(CONTEST_ID_KEY)) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": JSON input missing 'id' key: '" + jsonInputString + "'"); + // return HTTP 400 response code per CLICS spec + return Response.status(Status.BAD_REQUEST).entity("Missing '" + CONTEST_ID_KEY + "' key in " + contestsEndpoint).build(); + } + + // validate id + // TODO can the contestIdentifier be null? Yes, but it may be something else too. The CDS gives 'null', + // and it is unclear what other CCS's that we are shadowing for may provide. It is almost + // certainly NOT what PC2 set up as the identifier (Default-###############). As such, until the + // API endpoints are fixed to include a (configurable) contest identifier, a reasonable thing to + // do at this point is not validate the id at all. Just make sure one was specified (above). That's + // enough for now. + String jsonIdShorthand = LOG_PREFIX + contestId + ": JSON '" + CONTEST_ID_KEY + "' key "; + String idAsk = requestMap.get(CONTEST_ID_KEY); + + if(idAsk == null) { + controller.getLog().log(Log.WARNING, jsonIdShorthand + "is - we are accepting this non-compliant client's request"); + } else if(idAsk == "null") { + // We have seen a CDS supply the actual string "null", so we will make believe it is null and accept it. + controller.getLog().log(Log.WARNING, jsonIdShorthand + "is the word 'null' - we are accepting this non-compliant client's request"); + } else if(idAsk.equals(contestId) == false) { + controller.getLog().log(Log.WARNING, jsonIdShorthand + "'" + idAsk + "' does not match the URL contestId '" + contestId + "'"); + // return HTTP 409 response - client is confused and sending conflicting contest id's + return Response.status(Status.CONFLICT).entity("Invalid '" + CONTEST_ID_KEY + "' key in " + contestsEndpoint + " (non-complaint client)").build(); + } else if (!model.getContestIdentifier().equals(idAsk)) { + controller.getLog().log(Log.WARNING, jsonIdShorthand + "'" + idAsk + "' does not match the PC2 contest ID '" + model.getContestIdentifier() + "'"); + // return HTTP 409 - client is confused and/or non-compliant + return Response.status(Status.CONFLICT).entity("Invalid '" + CONTEST_ID_KEY + "' key in " + contestsEndpoint + " (non-complaint client)").build(); + } + + // get the Object corresponding to "start_time" + String startTimeValueString = null; + // get the countdown_pause_time key, if there + String countdownPauseTime = null; + // Flag to indicate that countdown pause time was specified + boolean sawCountdownPauseTime = false; + + // if we get here then the JSON parsed correctly; see if it contained "start_time" as a key (that is required by spec) + if (!requestMap.containsKey(CONTEST_START_TIME_KEY)) { + + // check if thaw time is present + if(!requestMap.containsKey(CONTEST_THAW_TIME)) { + // no, neither one is included. This is an error. + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": JSON input missing '" + CONTEST_START_TIME_KEY + "' key or '" + CONTEST_THAW_TIME + "' key: '" + jsonInputString + "'"); + // return HTTP 400 response code + return Response.status(Status.BAD_REQUEST).entity("Missing '" + CONTEST_START_TIME_KEY + "' key or '" + CONTEST_THAW_TIME + "' key in " + contestsEndpoint).build(); + } + return(HandleContestThawTime(sc, contestId, requestMap.get(CONTEST_THAW_TIME))); + } + + // its either a contest start time adjustment or a countdown pause adjustment + startTimeValueString = requestMap.get(CONTEST_START_TIME_KEY); + + if(requestMap.containsKey(CONTEST_COUNTDOWN_PAUSE_TIME_KEY)) { + sawCountdownPauseTime = true; + countdownPauseTime = requestMap.get(CONTEST_COUNTDOWN_PAUSE_TIME_KEY); + } + + // check for count down pause time + if(countdownPauseTime != null) { + // can't have both a start time and count down pause time + if(startTimeValueString != null) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": only one of '" + CONTEST_START_TIME_KEY + "' key or '" + CONTEST_COUNTDOWN_PAUSE_TIME_KEY + "' may be specified"); + // return HTTP 400 response + return Response.status(Status.BAD_REQUEST).entity("Only one of '" + CONTEST_START_TIME_KEY + "' key or '" + CONTEST_COUNTDOWN_PAUSE_TIME_KEY + "' may be specified for " + contestsEndpoint).build(); + } + return(HandleContestCountdownPauseTime(sc, contestId, countdownPauseTime)); + } + return(HandleContestStartTime(sc, contestId, startTimeValueString, sawCountdownPauseTime)); + } + + /** + * Process contest start time + * + * @param sc + * @param contestId which contest + * @param startTimeValueString new contest start time (ISO format) or null to make it undefined + * @param sawCountdownPauseTime + * @return web response + */ + private Response HandleContestStartTime(SecurityContext sc, String contestId, String startTimeValueString, boolean sawCountdownPauseTime) { + + StartTimeRequestType requestType = StartTimeRequestType.ILLEGAL; + GregorianCalendar requestedStartTime = null; + String logString = LOG_PREFIX + contestId + ": received '" + CONTEST_START_TIME_KEY + "': "; + + // check authorization (verify requester is allowed to make this request) + if (!isContestStartAllowed(sc)) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": unauthorized request"); + // return HTTP 401 response code per CLICS spec + return Response.status(Status.UNAUTHORIZED).entity("You are not authorized to access this page").build(); + } + // check if we have a start_time string (really? check for "null"?) + if (startTimeValueString == null || startTimeValueString.trim().equalsIgnoreCase("null")) { + requestType = StartTimeRequestType.SET_START_TO_UNDEFINED; + logString += ""; + } else { + logString += startTimeValueString; + + // parse the starttime value for a valid date + requestedStartTime = getDate(contestId, startTimeValueString); + if (requestedStartTime != null) { + requestType = StartTimeRequestType.SET_START_TO_SPECIFIED_DATE; + // } else { + // //null requestedStartTime means startTimeValueString failed to parse (wasn't a legal Unix epoch date); + // // do nothing -- leaving requestType set to "ILLEGAL" + } + } + controller.getLog().log(Log.DEBUG, logString + " Request Type: " + requestType.toString()); + + if (requestType == StartTimeRequestType.ILLEGAL) { + + // we can get here if the value was not "null" but also didn't parse to a valid date + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": JSON input contains invalid starttime value: '" + startTimeValueString + "'"); + // return HTTP 400 response code per CLICS spec + return Response.status(Status.BAD_REQUEST).entity("Bad value in starttime request").build(); + + } + + // we have a legal request; check to insure contest has not already been started + if (model.getContestTime().isContestStarted()) { + // contest has started, cannot set scheduled start time + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": request to set start time when contest has already started; ignored"); + // return HTTP 403 (Forbidden) response code per CLICS spec + return Response.status(Status.FORBIDDEN).entity("Contest already started").build(); + } + + // get the scheduled start time and the current time + GregorianCalendar scheduledStartTime = model.getContestInformation().getScheduledStartTime(); + GregorianCalendar now = new GregorianCalendar(); + // validate scheduleStartTime + // the contest has not yet started, but see if the scheduledStartTime was in the past + if (scheduledStartTime != null && scheduledStartTime.before(now)) { + // then clear it + scheduledStartTime = null; + } + boolean success = false; + + String minSecsToStart = "" + MIN_MS_TO_START_OF_CONTEST/1000 + " seconds"; + + switch (requestType) { + + case SET_START_TO_UNDEFINED: + + // check for less than 30 secs to scheduled start + if (scheduledStartTime != null && scheduledStartTime.getTimeInMillis() < (now.getTimeInMillis() + MIN_MS_TO_START_OF_CONTEST)) { + + // we have request to set start to "null", but we have a scheduled start and we're + // within 10 secs of it; cannot set scheduled start time to undefined (per CLICS spec); + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": received request to set start time to 'null' with less than " + minSecsToStart + " to go before start; ignored"); + // return HTTP 403 (Forbidden) response code per CLICS spec + return Response.status(Status.FORBIDDEN).entity("Cannot change start time to 'null' within " + minSecsToStart + " of already-scheduled start").build(); + + } else { + + // ok to set scheduled start to "undefined" + controller.getLog().log(Log.INFO, LOG_PREFIX + contestId + ": setStarttime(): setting contest start time to \"null\"."); + success = setScheduledStart(null, sawCountdownPauseTime); + if (success) { + return Response.ok().entity("Contest start time updated to \"null\" (no scheduled start)").build(); + } else { + controller.getLog().log(Log.SEVERE, LOG_PREFIX + contestId + ": setStarttime(): error setting contest start time to \"undefined\"."); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Server failed to set start time correctly").build(); + } + } + + // break; //can't get here, so Eclipse won't allow the explicit break + + case SET_START_TO_SPECIFIED_DATE: + + // check for less than 30 sec before scheduled start + if (scheduledStartTime != null && scheduledStartTime.getTimeInMillis() < (now.getTimeInMillis() + MIN_MS_TO_START_OF_CONTEST)) { + // we're within 30 secs of scheduled start; cannot set scheduled start time to new value (per CLICS spec); + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": received request to set start time with less than " + minSecsToStart + " to go before start; ignored"); + // return HTTP 403 (Forbidden) response code per CLICS spec + return Response.status(Status.FORBIDDEN).entity("Cannot change to new start time within " + minSecsToStart + " of already-scheduled start").build(); + } + + // check for less than 30 sec in the future + if (requestedStartTime.getTimeInMillis() < (now.getTimeInMillis() + MIN_MS_TO_START_OF_CONTEST)) { + + // requested start time is less than 30sec from now; cannot set (per CLICS spec); + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": received request to set start time less than " + minSecsToStart + " in the future; ignored"); + // return HTTP 403 (Forbidden) response code per CLICS spec + return Response.status(Status.FORBIDDEN).entity("Cannot set start time less than " + minSecsToStart + " in the future").build(); + } + + // ok to set scheduled start to a specific time + controller.getLog().log(Log.INFO, LOG_PREFIX + contestId + ": setStarttime(): setting contest start time to " + requestedStartTime); + success = setScheduledStart(requestedStartTime, sawCountdownPauseTime); + if (success) { + return Response.ok().entity("/contests/" + contestId).build(); + } else { + controller.getLog().log(Log.SEVERE, LOG_PREFIX + contestId + ": setStarttime(): error setting contest start time to requested date."); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Server failed to set start time correctly").build(); + } + + // break; + + default: + // shouldn't be able to get here! + controller.getLog().log(Log.SEVERE, LOG_PREFIX + contestId + ": setStarttime(): unknown default condition: request type = " + requestType); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Unknown condition in server: request type = " + requestType).build(); + } + } + + /** + * Process Contest count down pause + * + * @param sc User information + * @param contestId which contest + * @param countdownPauseTime how long before contest start should the count down pause (CLICS RELTIME value) + * @return web resposne + */ + private Response HandleContestCountdownPauseTime(SecurityContext sc, String contestId, String countdownPauseTime) { + + controller.getLog().log(Log.DEBUG, LOG_PREFIX + contestId + ": received '" + CONTEST_COUNTDOWN_PAUSE_TIME_KEY + "': " + countdownPauseTime); + + + // check authorization (verify requester is allowed to make this request) + if (!isContestStartAllowed(sc)) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": unauthorized request"); + // return HTTP 401 response code per CLICS spec + return Response.status(Status.UNAUTHORIZED).entity("You are not authorized to access this page").build(); + } + + long pauseTime = Utilities.convertCLICSContestTimeToMS(countdownPauseTime); + + // MIN_VALUE is returned on format error + if(pauseTime != Long.MIN_VALUE) { + // want to stop the countdown with this many milliseconds left + // TODO: tell PC2 to stop countdown when clock is 'pauseTime' ms away from start + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": countdown_pause_time not implemented"); + return Response.status(Status.NOT_MODIFIED).entity("Unable to set countdown_pause_time to " + pauseTime + " ms").build(); + } + return Response.status(Status.BAD_REQUEST).entity("Bad value for count down pause time request").build(); + } + + /** + * Process contest thaw time and generate response + * + * @param sc User information + * @param contestId which contest + * @param thawTimeValue ISO date of when the contest should unfreeze + * @return web response + */ + private Response HandleContestThawTime(SecurityContext sc, String contestId, String thawTimeValue) { + + // check authorization (verify requester is allowed to make this request) + if (!isContestThawAllowed(sc)) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": unauthorized request"); + // return HTTP 401 response code per CLICS spec + return Response.status(Status.UNAUTHORIZED).entity("You are not authorized to access this page").build(); + } + + // thaw time present, validate now + GregorianCalendar thawTime = getDate(contestId, thawTimeValue); + if (thawTime != null) { + // Set thaw time to this time. + // TODO: tell PC2 to thaw the contest at the given time. + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": setting of contest thaw time is not implemented"); + return Response.status(Status.NOT_MODIFIED).entity("Unable to set contest thaw time to " + thawTime.toString()).build(); + } + return Response.status(Status.BAD_REQUEST).entity("Bad value for contest thaw time request").build(); + + } + + /** + * Parses the given String and returns a {@link GregorianCalendar} object if the String represents a valid Unix Epoch date; otherwise returns null. + * + * @param contestId contest identifier + * @param startTimeValueString + * a String containing a date in ISO 8601 format. + * @return the GregorianCalendar date/time represented by the String, or null if the String does not represent a valid date/time + */ + private GregorianCalendar getDate(String contestId, String startTimeValueString) { + GregorianCalendar theDate = new GregorianCalendar(); + try { + theDate.setTime(Utilities.getIso8601formatterWithMS().parse(startTimeValueString)); + } catch (ParseException e) { + try { + controller.getLog().log(Log.DEBUG, LOG_PREFIX + contestId + ": Re-parsing date without MS " + startTimeValueString); + theDate.setTime(Utilities.getIso8601formatter().parse(startTimeValueString)); + } catch (ParseException e2) { + controller.getLog().throwing("ContestService", "getDate", e2); + return null; + } + } + + // debug + // System.out.println ("ContestService.getDate(): returning a GregorianCalendar with a date of " + theDate.getTimeInMillis()); + controller.getLog().log(Log.DEBUG, LOG_PREFIX + contestId + ": getDate(): returning a GregorianCalendar with a start date of " + theDate.getTimeInMillis()); + + return theDate; + } + + /** + * This method updates the Scheduled Start Date for the contest, including causing the scheduling of a "start contest" task for the specified date (which is assumed to be a valid date in the + * future). + * + * This is accomplished by telling the controller to update the {@link ContestInformation} with the scheduled start date. The controller then sends a packet to the server to do that; the server in + * turn creates a task to start the contest at the specified date/time. + * + * @param theDate + * the date/time to which the automatic start of the contest should be set, or null if the start date/time should be set to "undefined" + * @param unPauseCountdown tell pc2 that the countdown pause (if in effect) should be cancelled, and countdown should resume, if there's a valid start time + * @return true if the method was successful in setting the scheduled start time; false otherwise + */ + private boolean setScheduledStart(GregorianCalendar theDate, boolean unPauseCountdown) { + + // get the local model's ContestInformation + ContestInformation ci = model.getContestInformation(); + if (ci != null) { + // set the new start date/time into the ContestInformation + ci.setScheduledStartTime(theDate); + if (theDate != null) { + // if we have a valid start date, set the contest to auto-start + ci.setAutoStartContest(true); + } + // TODO unpause countdown - this will be a flag in "ci" + // tell the Controller to update the ContestInformation + controller.updateContestInformation(ci); + return true; + } else { + // for some reason we failed to get ContestInformation + return false; + } + } + + /** + * Converts the input string, assumed to be a JSON string, into a {@link Map} of JSON key-value pairs. + * + * @param contestId contest identifier + * @param jsonRequestString + * a JSON string specifying a starttime request in CLICS format + * @return a Map of the JSON string key-to-value pairs as Strings, or null if the input JSON does not parse as a Map(String->String). + */ + private Map parseJSONIntoMap(String contestId, String jsonRequestString) { + + controller.getLog().log(Log.INFO, LOG_PREFIX + contestId + ": parseJSONIntoMap(): attempting to convert JSON input '" + jsonRequestString + "' into Map"); + + // use Jackson's ObjectMapper to construct a Map of Strings-to-Strings from the JSON input + final ObjectMapper mapper = new ObjectMapper(); + final MapType mapType = mapper.getTypeFactory().constructMapType(Map.class, String.class, String.class); + final Map jsonDataMap; + + try { + jsonDataMap = mapper.readValue(jsonRequestString, mapType); + } catch (JsonMappingException e) { + // error parsing JSON input + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": parseJSONIntoMap(): JsonMappingException parsing JSON input '" + jsonRequestString + "'", e); + return null; + } catch (IOException e) { + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": parseJSONIntoMap(): IOException parsing JSON input '" + jsonRequestString + "'", e); + return null; + } + + return jsonDataMap; + } + + /** + * This method returns a representation of the current contest scheduled start time in JSON format as described on the CLICS wiki. + * + * @return a {@link Response} object containing a JSON String giving the scheduled contest start time as a Unix Epoch value, or as the string "undefined" if no start time is currently scheduled. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getContests() { + CLICSContestInfo [] allContests = new CLICSContestInfo[1]; + allContests[0] = new CLICSContestInfo(model); + try { + ObjectMapper mapper = JSONUtilities.getObjectMapper(); + String json = mapper.writeValueAsString(allContests); + return Response.ok(json, MediaType.APPLICATION_JSON).build(); + } catch (Exception e) { + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error creating JSON for contest info " + e.getMessage()).build(); + } + } + + /** + * Returns a response with the information for the specified contestId + * + * contestId contest for which information is requested + */ + @GET + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{contestId}/") + public Response getContest(@PathParam("contestId") String contestId) { + + // check contest id + if(contestId.equals(model.getContestIdentifier()) == true) { + try { + ObjectMapper mapper = JSONUtilities.getObjectMapper(); + String json = mapper.writeValueAsString(new CLICSContestInfo(model)); + return Response.ok(json, MediaType.APPLICATION_JSON).build(); + } catch (Exception e) { + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error creating JSON for contest info " + e.getMessage()).build(); + } + } + + return Response.status(Response.Status.NOT_FOUND).build(); + } + + /** + * Check the user has a role than change contest start time + * + * @param sc Security context for the user + * @return true if the user can perform the operation + */ + public static boolean isContestStartAllowed(SecurityContext sc) { + return(sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN)); + } + + /** + * Check the user has a role than change contest thaw time + * + * @param sc Security context for the user + * @return true if the user can perform the operation + */ + public static boolean isContestThawAllowed(SecurityContext sc) { + return(sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN)); + } + + /** + * Retrieve access information about this endpoint for the supplied user's security context + * + * @param sc User's security information + * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise + */ + public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { + return(new CLICSEndpoint("contest", JSONUtilities.getJsonProperties(CLICSContestInfo.class))); + } + + @Override + public boolean configure(FeatureContext arg0) { + // TODO Auto-generated method stub + return false; + } +} diff --git a/src/edu/csus/ecs/pc2/clics/API202306/EventFeedService.java b/src/edu/csus/ecs/pc2/clics/API202306/EventFeedService.java index 45ef51ad7..55dd29cab 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/EventFeedService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/EventFeedService.java @@ -1,9 +1,5 @@ // Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. -<<<<<<<< HEAD:src/edu/csus/ecs/pc2/clics/API202306/EventFeedService.java package edu.csus.ecs.pc2.clics.API202306; -======== -package edu.csus.ecs.pc2.clics.API202003; ->>>>>>>> 78682bd20 (i823 Relocate CLICS API Service endpoints):src/edu/csus/ecs/pc2/clics/API202003/EventFeedService.java import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -38,7 +34,7 @@ /** * Implementation of CLICS REST event-feed. - * + * * @author Douglas A. Lane, PC^2 Team, pc2@ecs.csus.edu */ @Path("/contest/event-feed") @@ -67,12 +63,12 @@ public EventFeedService(IInternalContest inContest, IInternalController inContro /** * a JSON stream representation of the events occurring in the contest. - * + * * @param type * a comma-separated query parameter identifying the type(s) of events being requested (if empty or null, indicates ALL event types) * @param id * the event-id of the earliest event being requested (i.e., an indication of the requested starting point in the event stream) - * + * * @return a {@link Response} object whose body contains the JSON event feed * @param asyncResponse * @param servletRequest @@ -96,7 +92,7 @@ public void streamEventFeed(@QueryParam("types") String eventTypeList, @QueryPar } EventFeedFilter filter = new EventFeedFilter(); - + if (eventTypeList != null) { filter.addEventTypeList(eventTypeList); System.out.println("starting event feed, sending only event types '" + eventTypeList + "'"); @@ -153,10 +149,10 @@ public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub return false; } - + /** * Create a snapshot of the JSON event feed. - * + * * @param contest * @param controller * @return diff --git a/src/edu/csus/ecs/pc2/clics/API202306/GroupService.java b/src/edu/csus/ecs/pc2/clics/API202306/GroupService.java index 25bcb9049..99f68211a 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/GroupService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/GroupService.java @@ -2,7 +2,6 @@ package edu.csus.ecs.pc2.clics.API202306; import java.util.ArrayList; - import javax.inject.Singleton; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -14,11 +13,9 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.SecurityContext; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.model.Group; import edu.csus.ecs.pc2.core.model.IInternalContest; @@ -47,9 +44,9 @@ public GroupService(IInternalContest inContest, IInternalController inController } /** - * This method returns a representation of the current contest groups in JSON format. + * This method returns a representation of the current contest groups in JSON format. * The response is a JSON array with one group description per array element, complying with 2023-06 - * + * * @param contestId The contest id * @return a {@link Response} object containing the groups in JSON form */ @@ -59,11 +56,11 @@ public Response getGroups(@PathParam("contestId") String contestId) { // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } ArrayList glist = new ArrayList(); - + for(Group group: model.getGroups()) { if (group.isDisplayOnScoreboard()) { glist.add(new CLICSGroup(group)); @@ -77,12 +74,12 @@ public Response getGroups(@PathParam("contestId") String contestId) { return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error creating JSON for groups " + e.getMessage()).build(); } } - + /** - * Returns a representation of the specified groupid in the specified contestid. + * Returns a representation of the specified groupid in the specified contestid. * The response is a JSON object describing the group, complying with 2023-06 - * + * * @param contestId The contest * @param groupId The desired group in the contest * @return a {@link Response} object containing the groups in JSON form @@ -113,16 +110,6 @@ public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { return(new CLICSEndpoint("groups", JSONUtilities.getJsonProperties(CLICSGroup.class))); } - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("groups", JSONUtilities.getJsonProperties(CLICSGroup.class))); - } - @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/JudgementService.java b/src/edu/csus/ecs/pc2/clics/API202306/JudgementService.java index 9856d1dd8..c9ff98366 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/JudgementService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/JudgementService.java @@ -15,8 +15,8 @@ import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.ObjectMapper; @@ -33,7 +33,7 @@ /** * WebService to handle judgements endpoint - * + * * @author John Buck * */ @@ -56,7 +56,7 @@ public JudgementService(IInternalContest inContest, IInternalController inContro /** * This method returns a representation of judgments for the specified contest in JSON format. The returned value is a JSON array with one judgment description per array element, complying with 2023-06 - * + * * @param sc User's information * @param contestId The contest * @return a {@link Response} object containing the contest judgments in JSON form @@ -64,18 +64,18 @@ public JudgementService(IInternalContest inContest, IInternalController inContro @GET @Produces(MediaType.APPLICATION_JSON) public Response getJudgements(@Context SecurityContext sc, @PathParam("contestId") String contestId) { - + // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } - + long freezeTime = Utilities.getFreezeTime(model); Set exceptProps = new HashSet(); StringJoiner allJudgments = new StringJoiner(","); ObjectMapper mapper = JSONUtilities.getObjectMapper(); CLICSRun cRun; - + for (Run run: model.getRuns()) { // If not admin or judge, can not see runs after freeze time if (!sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) && !sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE)) { @@ -101,7 +101,7 @@ public Response getJudgements(@Context SecurityContext sc, @PathParam("contestId /** * Returns a representation of a specified judgment for the specified contest in JSON format. The returned value compliant with 2023-06 - * + * * @param sc User's infor * @param contestId The contest * @param judgementId The judgement we're looking for @@ -127,7 +127,7 @@ public Response getJudgement(@Context SecurityContext sc, @PathParam("contestId" if (run.getElementId().toString().equals(judgementId)) { Set exceptProps = new HashSet(); CLICSRun cRun = new CLICSRun(model, run, exceptProps); - try { + try { ObjectMapper mapper = JSONUtilities.getObjectMapper(); // create filter to omit unused/bad properties (location, for example) SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.serializeAllExcept(exceptProps); @@ -153,16 +153,6 @@ public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { return(new CLICSEndpoint("judgements", JSONUtilities.getJsonProperties(CLICSRun.class))); } - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("judgements", JSONUtilities.getJsonProperties(CLICSRun.class))); - } - @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/JudgementTypeService.java b/src/edu/csus/ecs/pc2/clics/API202306/JudgementTypeService.java index 8fc36bddd..52f444e43 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/JudgementTypeService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/JudgementTypeService.java @@ -12,7 +12,6 @@ import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.ext.Provider; @@ -26,9 +25,9 @@ import edu.csus.ecs.pc2.services.core.JSONUtilities; /** - * WebService to handle languages + * WebService to handle Judgement Types * - * @author ICPC + * @author John Buck * */ @Path("/contests/{contestId}/judgement-types") @@ -48,7 +47,7 @@ public JudgementTypeService(IInternalContest inContest, IInternalController inCo } /** - * This method returns a representation of the current contest groups in JSON format. The returned value is a JSON array with one judgement-type description per array element, complying with 2023-06 + * This method returns a representation of the current contest judgement types in JSON format. The returned value is a JSON array with one judgement-type description per array element, complying with 2023-06 * * @param contestId contest for which judgment types are requested * @return a {@link Response} object containing the contest judgement-types in JSON form @@ -107,16 +106,6 @@ public Response getJudgementType(@PathParam("contestId") String contestId, @Path } return Response.status(Response.Status.NOT_FOUND).build(); } - - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("judgement-types", JSONUtilities.getJsonProperties(CLICSJudgmentType.class))); - } /** * Retrieve access information about this endpoint for the supplied user's security context diff --git a/src/edu/csus/ecs/pc2/clics/API202306/LanguageService.java b/src/edu/csus/ecs/pc2/clics/API202306/LanguageService.java index 6739027f1..16776590f 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/LanguageService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/LanguageService.java @@ -14,19 +14,17 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.SecurityContext; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.csus.ecs.pc2.core.IInternalController; -import edu.csus.ecs.pc2.core.model.IInternalContest; import edu.csus.ecs.pc2.core.model.Language; import edu.csus.ecs.pc2.services.core.JSONUtilities; +import edu.csus.ecs.pc2.core.model.IInternalContest; /** * WebService to handle languages - * + * * @author John Buck * */ @@ -50,21 +48,21 @@ public LanguageService(IInternalContest inContest, IInternalController inControl /** * Returns a representation of the current model languages in JSON format. The returned value is a JSON array with one language description per array element, complying with 2023-06 - * + * * @param contestId The contest * @return a {@link Response} object containing the model languages in JSON form */ @GET @Produces(MediaType.APPLICATION_JSON) public Response getLanguages(@PathParam("contestId") String contestId) { - + // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } - + ArrayList llist = new ArrayList(); - + // get the languages, one-at-a-time from the model for(Language language: model.getLanguages()) { if (language.isActive()) { @@ -82,7 +80,7 @@ public Response getLanguages(@PathParam("contestId") String contestId) { /** * Returns a representation of the specified language for the specified contest in JSON format. The returned value is compliant with 2023-06 - * + * * @param contestId The contest * @param languageId The language * @return @@ -91,7 +89,7 @@ public Response getLanguages(@PathParam("contestId") String contestId) { @Produces(MediaType.APPLICATION_JSON) @Path("{languageId}/") public Response getLanguage(@PathParam("contestId") String contestId, @PathParam("languageId") String languageId) { - + // get the languages, one-at-a-time from the model for(Language language: model.getLanguages()) { if (language.isActive() && language.getElementId().toString().equals(languageId)) { @@ -111,16 +109,6 @@ public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { return(new CLICSEndpoint("languages", JSONUtilities.getJsonProperties(CLICSLanguage.class))); } - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("languages", JSONUtilities.getJsonProperties(CLICSLanguage.class))); - } - @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/OrganizationService.java b/src/edu/csus/ecs/pc2/clics/API202306/OrganizationService.java index 326012b64..328af6370 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/OrganizationService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/OrganizationService.java @@ -13,7 +13,6 @@ import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.ext.Provider; @@ -30,7 +29,7 @@ import edu.csus.ecs.pc2.services.core.JSONUtilities; /** - * WebService for handling teams + * WebService for handling organizations * * @author John Buck * @@ -116,16 +115,6 @@ public Response getOrganization(@PathParam("contestId") String contestId, @PathP } return Response.status(Response.Status.NOT_FOUND).build(); } - - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("organizations", JSONUtilities.getJsonProperties(CLICSOrganization.class))); - } /** * Retrieve access information about this endpoint for the supplied user's security context diff --git a/src/edu/csus/ecs/pc2/clics/API202306/ProblemService.java b/src/edu/csus/ecs/pc2/clics/API202306/ProblemService.java index ce3ce9ed2..8d8256fde 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/ProblemService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/ProblemService.java @@ -13,12 +13,11 @@ import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.model.IInternalContest; import edu.csus.ecs.pc2.core.model.Problem; @@ -28,7 +27,7 @@ /** * WebService to handle problems - * + * * @author John Buck * */ @@ -54,7 +53,7 @@ public ProblemService(IInternalContest inContest, IInternalController inControll /** * This method returns a representation of the current contest problems in JSON format. The returned value is a JSON array with one problems description per array element, compying with 2023-06 - * + * * @param sc User information * @param contestId The contest * @return a {@link Response} object containing the contest problems in JSON form @@ -64,16 +63,16 @@ public ProblemService(IInternalContest inContest, IInternalController inControll public Response getProblems(@Context SecurityContext sc, @PathParam("contestId") String contestId) { System.err.println("getProblems from " + sc.getUserPrincipal().getName() + " for contest " + contestId); - + // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } // get the problems from the contest Problem[] problems = model.getProblems(); int ord = 1; ArrayList problist = new ArrayList(); - + // public only gets the problems when the contest starts if (sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) || sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE) || model.getContestTime().isContestStarted()) { for (Problem problem: problems) { @@ -94,7 +93,7 @@ public Response getProblems(@Context SecurityContext sc, @PathParam("contestId") /** * Return the reponse to a request for a single problem's information for the specified contest and problem id. - * + * * @param sc user info * @param contestId the contest * @param problemId the problem id desired @@ -116,7 +115,7 @@ public Response getProblem(@Context SecurityContext sc, @PathParam("contestId") try { return Response.ok(JSONUtilities.getObjectMapper().writeValueAsString(new CLICSProblem(model, problem, ord)), MediaType.APPLICATION_JSON).build(); } catch(Exception e) { - return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error creating JSON for problem " + problemId + " in contest " + contestId + ": " + e.getMessage()).build(); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error creating JSON for problem " + problemId + " in contest " + contestId + ": " + e.getMessage()).build(); } } ord++; @@ -126,10 +125,10 @@ public Response getProblem(@Context SecurityContext sc, @PathParam("contestId") } return Response.status(Response.Status.FORBIDDEN).build(); } - + /** * Retrieve access information about this endpoint for the supplied user's security context - * + * * @param sc User's security information * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise */ diff --git a/src/edu/csus/ecs/pc2/clics/API202306/ResourceConfig202306.java b/src/edu/csus/ecs/pc2/clics/API202306/ResourceConfig202306.java index c103ccfaf..e3f1c47a0 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/ResourceConfig202306.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/ResourceConfig202306.java @@ -21,24 +21,23 @@ public class ResourceConfig202306 implements ICLICSResourceConfig { public static final String CLICS_API_VERSION = "2023-06"; public static final String CLICS_API_VERSION_URL = "https://ccs-specs.icpc.io/2023-06/contest_api"; - + private IInternalContest contest; - + private IInternalController controller; - + private WebServerPropertyUtils wsPropUtilities; private Log log = null; - + /** * {@inheritDoc} */ - @Override public ResourceConfig getResourceConfig(IInternalContest aContest, IInternalController aController, WebServerPropertyUtils wsPropUtil) { setContestAndController(aContest, aController); wsPropUtilities = wsPropUtil; - + // create and (empty) ResourceConfig ResourceConfig resConfig = new ResourceConfig(); resConfig.register(RolesAllowedDynamicFeature.class); diff --git a/src/edu/csus/ecs/pc2/clics/API202306/RunService.java b/src/edu/csus/ecs/pc2/clics/API202306/RunService.java index a856170f0..dada65c33 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/RunService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/RunService.java @@ -2,7 +2,6 @@ package edu.csus.ecs.pc2.clics.API202306; import java.util.ArrayList; - import javax.inject.Singleton; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -13,12 +12,11 @@ import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.Utilities; import edu.csus.ecs.pc2.core.model.IInternalContest; @@ -29,7 +27,7 @@ /** * WebService to handle runs - * + * * @author John Buck * */ @@ -59,16 +57,16 @@ public RunService(IInternalContest inContest, IInternalController inController) @GET @Produces(MediaType.APPLICATION_JSON) public Response getRuns(@Context SecurityContext sc, @PathParam("contestId") String contestId) { - + // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } - + long freezeTime = Utilities.getFreezeTime(model); - + ArrayList tclist = new ArrayList(); - + for (Run run: model.getRuns()) { // If not admin or judge, can not see runs after freeze time if (!sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) && !sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE)) { @@ -94,7 +92,7 @@ public Response getRuns(@Context SecurityContext sc, @PathParam("contestId") Str /** * Returns a representation of the specified test case in the specified contest in JSON format. The returned value is compliant with 2023-06 API. - * + * * @param sc User's info * @param contestId The contest * @param runId The run of interest @@ -108,7 +106,7 @@ public Response getRun(@Context SecurityContext sc, @PathParam("contestId") Stri // check contest id if(contestId.equals(model.getContestIdentifier()) == true) { long freezeTime = Utilities.getFreezeTime(model); - + for (Run run: model.getRuns()) { // If not admin or judge, can not see runs after freeze time if (!sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) && !sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE)) { @@ -139,16 +137,6 @@ public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { return(new CLICSEndpoint("runs", JSONUtilities.getJsonProperties(CLICSTestCase.class))); } - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("runs", JSONUtilities.getJsonProperties(CLICSTestCase.class))); - } - @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/ScoreboardService.java b/src/edu/csus/ecs/pc2/clics/API202306/ScoreboardService.java index 3bafe646e..95f53cc61 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/ScoreboardService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/ScoreboardService.java @@ -32,7 +32,7 @@ import edu.csus.ecs.pc2.services.eventFeed.WebServer; /** * Webservice to handle scoreboard requests - * + * * @author ICPC */ @Path("/contests/{contestId}/scoreboard") @@ -51,9 +51,9 @@ public ScoreboardService(IInternalContest inContest, IInternalController inContr } /** - * This method returns a representation of the current contest scoreboard in JSON format. + * This method returns a representation of the current contest scoreboard in JSON format. * The return JSON is in the format defined by the CLICS 2023-06 spec. - * + * * @param servletRequest * @param sc * @param contestId @@ -73,10 +73,10 @@ public Response getScoreboard(@Context HttpServletRequest servletRequest, @Conte ((sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN) || sc.isUserInRole(WebServer.WEBAPI_ROLE_ANALYST) || sc.isUserInRole(WebServer.WEBAPI_ROLE_JUDGE)))) { - + Group specificGroup = null; Integer divNumber = null; - + // if a specific group was requested, let's look for that so we can pass it to the standings routine if(!StringUtilities.isEmpty(group_id)) { for(Group group: model.getGroups()) { @@ -86,10 +86,10 @@ public Response getScoreboard(@Context HttpServletRequest servletRequest, @Conte } } if(specificGroup == null) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } } - + // see if a division number was specified - this is a PC2 extension(!!) and is NOT part of the CLICS API, but, // we are allowed to add things. if(!StringUtilities.isEmpty(division)) { @@ -97,19 +97,19 @@ public Response getScoreboard(@Context HttpServletRequest servletRequest, @Conte divNumber = Integer.parseInt(division); } catch(Exception e) { // Bad division specified - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } } - + // ok to return scoreboard try { - CLICSScoreboard scoreboard = new CLICSScoreboard(model, controller, specificGroup, divNumber); + CLICSScoreboard scoreboard = new CLICSScoreboard(model, controller, specificGroup, divNumber); return Response.ok(scoreboard.toJSON(), MediaType.APPLICATION_JSON).build(); } catch (IllegalContestState | JAXBException | IOException e) { controller.getLog().log(Log.WARNING, "Exception creating PC2 scoreboard JSON: " + e.getMessage(), e); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } - + } else { // do not show (return) the scoreboard if the contest has not // been started and the requester is not special) @@ -117,12 +117,12 @@ public Response getScoreboard(@Context HttpServletRequest servletRequest, @Conte return Response.status(Status.FORBIDDEN).build(); } } - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } - + /** * Retrieve access information about this endpoint for the supplied user's security context - * + * * @param sc User's security information * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise */ diff --git a/src/edu/csus/ecs/pc2/clics/API202306/StateService.java b/src/edu/csus/ecs/pc2/clics/API202306/StateService.java index 95215ebdf..ef6d98250 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/StateService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/StateService.java @@ -20,7 +20,7 @@ /** * WebService to handle "state" REST endpoint as described by the CLICS wiki. - * + * * @author pc2@ecs.csus.edu * */ @@ -42,7 +42,7 @@ public StateService(IInternalContest inModel, IInternalController inController) /** * This method returns a representation of the current contest state in JSON format as described on the CLICS wiki. - * + * * @param sc user information * @param contestId The contest * @return a {@link Response} object containing a JSON String giving the scheduled contest start time as a Unix Epoch value, or as the string "undefined" if no start time is currently scheduled. @@ -53,7 +53,7 @@ public Response getState(@Context SecurityContext sc, @PathParam("contestId") St // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } return Response.ok(new CLICSContestState(model).toJSON(), MediaType.APPLICATION_JSON).build(); } @@ -68,16 +68,6 @@ public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { return(new CLICSEndpoint("state", JSONUtilities.getJsonProperties(CLICSContestState.class))); } - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("state", JSONUtilities.getJsonProperties(CLICSContestState.class))); - } - @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java b/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java index fcdac299c..632a5ee24 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java @@ -36,8 +36,6 @@ import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.log.Log; -import edu.csus.ecs.pc2.core.model.IInternalContest; -import edu.csus.ecs.pc2.core.model.IRunListener; import edu.csus.ecs.pc2.core.model.Run; import edu.csus.ecs.pc2.core.model.RunEvent; import edu.csus.ecs.pc2.core.model.RunFiles; @@ -45,10 +43,12 @@ import edu.csus.ecs.pc2.core.security.FileSecurityException; import edu.csus.ecs.pc2.core.util.JSONTool; import edu.csus.ecs.pc2.services.eventFeed.WebServer; +import edu.csus.ecs.pc2.core.model.IInternalContest; +import edu.csus.ecs.pc2.core.model.IRunListener; /** * WebService to handle submissions - * + * * @author ICPC * */ @@ -78,37 +78,33 @@ public SubmissionService(IInternalContest inContest, IInternalController inContr /** * Run Listener - * + * * @author pc2@ecs.csus.edu */ public class RunListenerImplementation implements IRunListener { - @Override public void runAdded(RunEvent event) { // ignore } - @Override public void refreshRuns(RunEvent event) { // ignore } - @Override public void runChanged(RunEvent event) { // server replied, aka our model has been updated :) serverReplied = true; } - @Override public void runRemoved(RunEvent event) { // ignore } } /** - * This method returns a JSON representation of all Runs (Submissions). - * + * This method returns a JSON representation of all Runs (Submissions). + * * @return a {@link Response} object containing the Submissions in JSON form */ @GET @@ -149,17 +145,17 @@ public Response getSubmissionFiles(@Context SecurityContext sc, @PathParam("subm for (int i = 0; i < runs.length; i++) { Run submission = runs[i]; if (jsonTool.getSubmissionId(submission).equals(submissionId)) { - + //we found the requested Submission ID in the list of runs returned from the model; try to get the runfiles for Submission runFiles = null; try { controller.getLog().log(Log.INFO, "Requesting run files for submission " + submission.getNumber() + " from local client model"); runFiles = model.getRunFiles(submission); } catch (ClassNotFoundException | IOException | FileSecurityException e2) { - controller.getLog().log(Log.INFO, "Exception attempting to get run files for submission " + controller.getLog().log(Log.INFO, "Exception attempting to get run files for submission " + submissionId + " from local model", e2); } - + //if runFiles is still null we failed to get the runfiles from the local model if (runFiles == null) { // try getting the submission from the server @@ -220,7 +216,7 @@ public Response getSubmissionFiles(@Context SecurityContext sc, @PathParam("subm createZip(submission, tmpDir, filesToWrite, zipFileName); // set file (and path) to be download File file = new File(zipFileName); - ResponseBuilder responseBuilder = Response.ok(file); + ResponseBuilder responseBuilder = Response.ok((Object) file); responseBuilder.header("Content-Disposition", "attachment; filename=\"files.zip\""); return responseBuilder.build(); } catch (IOException e) { @@ -263,7 +259,7 @@ private void createZip(Run submission, java.nio.file.Path tmpDir, HashMap iterator = filesToWrite.keySet().iterator(); iterator.hasNext();) { - Integer fileIndex = iterator.next(); + Integer fileIndex = (Integer) iterator.next(); String inputFile = filesToWrite.get(fileIndex); FileInputStream in = new FileInputStream(tmpDir + File.pathSeparator + inputFile); ZipEntry ze = new ZipEntry(inputFile); @@ -311,7 +307,7 @@ public FileVisitResult postVisitDirectory(java.nio.file.Path dir, IOException e) /** * Check if the supplied user has a team or admin role, if so they can make team submissions - * + * * @param sc User's security context * @return true if the user is allowed to make team submissions */ @@ -321,7 +317,7 @@ public static boolean isTeamSubmitAllowed(SecurityContext sc) { /** * Check if the supplied user has a admin, if so they can make submissions on behalf of a team - * + * * @param sc User's security context * @return true if the user is allowed to make team submissions */ @@ -331,14 +327,14 @@ public static boolean isProxySubmitAllowed(SecurityContext sc) { /** * Check if the supplied user has the admin role, if so they can make admin submissions - * + * * @param sc User's security context * @return true if the user is allowed to make team submissions */ public static boolean isAdminSubmitAllowed(SecurityContext sc) { return(sc.isUserInRole(WebServer.WEBAPI_ROLE_ADMIN)); } - + @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/TeamService.java b/src/edu/csus/ecs/pc2/clics/API202306/TeamService.java index 0e3731c8b..648fb7363 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/TeamService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/TeamService.java @@ -13,12 +13,11 @@ import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.model.Account; import edu.csus.ecs.pc2.core.model.ClientType; @@ -27,7 +26,7 @@ /** * WebService for handling teams - * + * * @author ICPC * */ @@ -49,7 +48,7 @@ public TeamService(IInternalContest inContest, IInternalController inController) /** * This method returns a representation of the current model teams in JSON format. The returned value is a JSON array with one team description per array element. - * + * * @param sc user information * @param contestId the contest for which the teams are requested * @return a {@link Response} object containing the model teams in JSON form @@ -60,7 +59,7 @@ public Response getTeams(@Context SecurityContext sc, @PathParam("contestId") St // check contest id if(contestId.equals(model.getContestIdentifier()) == false) { - return Response.status(Response.Status.NOT_FOUND).build(); + return Response.status(Response.Status.NOT_FOUND).build(); } // get the team accounts from the model Account[] accounts = model.getAccounts(); @@ -69,7 +68,7 @@ public Response getTeams(@Context SecurityContext sc, @PathParam("contestId") St ArrayList teams = new ArrayList(); for (int i = 0; i < accounts.length; i++) { Account account = accounts[i]; - + if (account.getClientId().getClientType().equals(ClientType.Type.TEAM)) { teams.add(new CLICSTeam(model, account)); } @@ -85,7 +84,7 @@ public Response getTeams(@Context SecurityContext sc, @PathParam("contestId") St /** * Return response to the request for information about a specific teamid in a specific contestid - * + * * @param sc user information * @param contestId the contest * @param teamId the team id @@ -100,7 +99,7 @@ public Response getTeam(@Context SecurityContext sc, @PathParam("contestId") Str if(contestId.equals(model.getContestIdentifier()) == true) { // get the team accounts from the model Account[] accounts = model.getAccounts(); - + for (int i = 0; i < accounts.length; i++) { Account account = accounts[i]; // TODO multi-site with overlapping teamNumbers? @@ -122,16 +121,6 @@ public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { return(new CLICSEndpoint("teams", JSONUtilities.getJsonProperties(CLICSTeam.class))); } - /** - * Retrieve access information about this endpoint for the supplied user's security context - * - * @param sc User's security information - * @return CLICSEndpoint object if the user can access this endpoint's properties, null otherwise - */ - public static CLICSEndpoint getEndpointProperties(SecurityContext sc) { - return(new CLICSEndpoint("teams", JSONUtilities.getJsonProperties(CLICSTeam.class))); - } - @Override public boolean configure(FeatureContext arg0) { // TODO Auto-generated method stub diff --git a/src/edu/csus/ecs/pc2/clics/API202306/VersionService.java b/src/edu/csus/ecs/pc2/clics/API202306/VersionService.java index b50d13aed..8fb0d210d 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/VersionService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/VersionService.java @@ -17,7 +17,7 @@ /** * Web Service to handle "version" REST endpoint as described by the CLICS wiki. - * + * * @author pc2@ecs.csus.edu * */ @@ -43,7 +43,7 @@ public VersionService(IInternalContest inModel, IInternalController inController /** * This method returns a representation of the current contest version in JSON format as described on the CLICS wiki. - * + * * @return a {@link Response} object containing a JSON String with API version information */ @GET diff --git a/src/edu/csus/ecs/pc2/ui/WebServerPane.java b/src/edu/csus/ecs/pc2/ui/WebServerPane.java index 40c905ff2..fa3f03adf 100644 --- a/src/edu/csus/ecs/pc2/ui/WebServerPane.java +++ b/src/edu/csus/ecs/pc2/ui/WebServerPane.java @@ -32,9 +32,6 @@ import edu.csus.ecs.pc2.services.eventFeed.WebServer; import edu.csus.ecs.pc2.services.eventFeed.WebServerPropertyUtils; import edu.csus.ecs.pc2.services.web.EventFeedStreamer; -import edu.csus.ecs.pc2.core.IniFile; -import edu.csus.ecs.pc2.core.StringUtilities; -import edu.csus.ecs.pc2.core.util.CommaSeparatedValueParser; /** * This class provides a GUI for configuring the embedded Jetty webserver. It allows specifying the port on which Jetty will listen and the REST service endpoints to which Jetty will respond. (Note @@ -54,14 +51,6 @@ public class WebServerPane extends JPanePlugin { public static final int DEFAULT_WEB_SERVER_PORT_NUMBER = WebServer.DEFAULT_WEB_SERVER_PORT_NUMBER; private static final String NL = System.getProperty("line.separator"); - - private static final String CLICS_VERSIONS_KEY = "clics.apiVersionsSupported"; - - private static final String [] DEF_CLICS_API_VERSIONS = { "2023-06", "2020-03" }; - - private static final String CLICS_VERSIONS_KEY = "clics.apiVersionsSupported"; - - private static final String [] DEF_CLICS_API_VERSIONS = { "2023-06", "2020-03" }; private static final String CLICS_VERSIONS_KEY = "clics.apiVersionsSupported";