diff --git a/bin/pc2submit b/bin/pc2submit index d8a3d3739..61167896f 100755 --- a/bin/pc2submit +++ b/bin/pc2submit @@ -522,12 +522,15 @@ def do_api_submit(): if team_id : jsonSub['team_id'] = team_id + + zip = io.BytesIO() + + with zipfile.ZipFile(zip, 'w', zipfile.ZIP_DEFLATED) as zipf: + for filename in filenames: + arcname = os.path.basename(filename) + zipf.write(filename, arcname) - jsonSub['files'] = [] - for filename in filenames : - with open(filename, 'rb') as file: - data = base64.b64encode(file.read()).decode("utf-8") - jsonSub['files'].append({ 'data' : data, 'filename' : filename }) + jsonSub['files'] = [{ 'data' : base64.b64encode(zip.getvalue()).decode("utf-8"), 'mime' : 'application/zip' }] logging.debug(f"API Post info: jsonSub: {jsonSub}") diff --git a/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java b/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java index 0d174bb1e..2f950abac 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/CLICSContestInfo.java @@ -107,6 +107,9 @@ public CLICSContestInfo(IInternalContest model, ContestInformation ci) { } } penalty_time = Integer.valueOf(ci.getScoringProperties().getProperty(DefaultScoringAlgorithm.POINTS_PER_NO, "20")); + if(ci.getThawed() != null) { + scoreboard_thaw_time = Utilities.getIso8601formatterWithMS().format(ci.getThawed()); + } scoreboard_type = "pass-fail"; } diff --git a/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java b/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java index d24cb7224..227b9aea1 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/ContestService.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.text.ParseException; +import java.util.Date; import java.util.GregorianCalendar; import java.util.Map; @@ -420,11 +421,22 @@ private Response HandleContestThawTime(SecurityContext sc, String contestId, Str // 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(); + Date thawDate = thawTime.getTime(); + String thawStr = Utilities.getIso8601formatterWithMS().format(thawDate); + // get the local model's ContestInformation sincd we are modifying the thaw time + ContestInformation ci = model.getContestInformation(); + if (ci != null) { + // set the new start date/time into the ContestInformation + ci.setThawed(thawDate); + // tell the Controller to update the ContestInformation, eg the thaw time in this case + controller.updateContestInformation(ci); + controller.getLog().log(Log.INFO, LOG_PREFIX + contestId + ": setting contest thaw time to " + thawStr); + return Response.ok().entity("Contest thaw time set to " + thawStr).build(); + } + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": can not get contest information to set thaw time"); + return Response.status(Status.NOT_MODIFIED).entity("Unable to set contest thaw time to " + thawStr).build(); } + controller.getLog().log(Log.WARNING, LOG_PREFIX + contestId + ": bad value for contest thaw time: " + thawTimeValue); return Response.status(Status.BAD_REQUEST).entity("Bad value for contest thaw time request").build(); } diff --git a/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java b/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java index 6c321ec33..90236a482 100644 --- a/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java +++ b/src/edu/csus/ecs/pc2/clics/API202306/SubmissionService.java @@ -49,6 +49,7 @@ import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import edu.csus.ecs.pc2.convert.EventFeedUtilities; import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.Utilities; import edu.csus.ecs.pc2.core.exception.SubmissionRejectedException; @@ -607,24 +608,44 @@ public synchronized Response addNewSubmission(@Context HttpServletRequest servle return Response.status(Response.Status.BAD_REQUEST).entity("no file specified").build(); } - List srcFiles = new ArrayList(); - for(CLICSFileReference file : files) { - String fileName = file.getFilename(); - if("".equals(fileName)) { - return Response.status(Response.Status.BAD_REQUEST).entity("no file name specified").build(); + List srcFiles; + + // Backward compatability test: If no mime property specified on the first file, then the CLICSFileReference's are + // actual files and not a zip file. This is in here because initially, due to a misinterpretation of the CLICS + // 2023-06 specification, the command line submit utility (a.k.a. pc2submit) did not put the files in a zip file, + // rather, it created an array of base64 encoded file contents. The CLICS spec has since been clarified but for now, + // we will still support the incorrect implementation as well as the correct one. + CLICSFileReference firstFile = files[0]; + String mimeType = firstFile.getMime(); + + // TODO: deprecate this "if" part and leave the "else" part. JB + if(mimeType == null || "".equals(mimeType)) { + srcFiles = new ArrayList(); + for(CLICSFileReference file : files) { + String fileName = file.getFilename(); + if("".equals(fileName)) { + return Response.status(Response.Status.BAD_REQUEST).entity("no file name specified").build(); + } + // allow contestant submission of a zero length file. This will generate a CE (hopefully). + // if the following code is uncommented, the submission is not made and a 400 is returned to the submitter. + // it appears that other CCS's allow zero length submissions. *sigh* -- JB + String fileData = file.getData(); + if(fileData == null || fileData.length() == 0) { + // nice to put it in the log in case any questions come up. + log.info(user + " POSTing empty source submission on behalf of team " + team_id); + + // return Response.status(Response.Status.BAD_REQUEST).entity("no file data specified for " + fileName).build(); + } + IFile iFile = new IFileImpl(file.getFilename(), fileData); + srcFiles.add(iFile); } - // allow contestant submission of a zero length file. This will generate a CE (hopefully). - // if the following code is uncommented, the submission is not made and a 400 is returned to the submitter. - // it appears that other CCS's allow zero length submissions. *sigh* -- JB - String fileData = file.getData(); - if(fileData == null || fileData.length() == 0) { - // nice to put it in the log in case any questions come up. - log.info(user + " POSTing empty source submission on behalf of team " + team_id); - -// return Response.status(Response.Status.BAD_REQUEST).entity("no file data specified for " + fileName).build(); + } else { + // There should be precisely one file in the files[] array, which is a zip archive of the source file(s) + if(files.length > 1) { + return Response.status(Response.Status.BAD_REQUEST).entity("only one zip archive is allowed").build(); } - IFile iFile = new IFileImpl(file.getFilename(), fileData); - srcFiles.add(iFile); + + srcFiles = EventFeedUtilities.getIFiles(firstFile.getData()); } String entry = sub.getEntry_point(); IFile mainFile = srcFiles.get(0); diff --git a/src/edu/csus/ecs/pc2/convert/EventFeedUtilities.java b/src/edu/csus/ecs/pc2/convert/EventFeedUtilities.java index 510f762c3..61219ac25 100644 --- a/src/edu/csus/ecs/pc2/convert/EventFeedUtilities.java +++ b/src/edu/csus/ecs/pc2/convert/EventFeedUtilities.java @@ -1,23 +1,31 @@ -// Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2025 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.convert; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import edu.csus.ecs.pc2.core.model.IFile; +import edu.csus.ecs.pc2.core.model.IFileImpl; /** * Event Feed Utilities - * + * * @author ICPC * */ public final class EventFeedUtilities { public static final long MS_PER_SECOND = 1000; - + private EventFeedUtilities() { super(); } @@ -29,7 +37,7 @@ public static String[] getAllLanguages(List runs) { map.put(eventFeedRun.getLanguage(), ""); } Set set = map.keySet(); - return (String[]) set.toArray(new String[set.size()]); + return set.toArray(new String[set.size()]); } public static int getMaxProblem(List runs) { @@ -55,7 +63,7 @@ public static int getMaxTeam(List runs) { /** * Convert decimal string to ms. - * + * * @param decimalSeconds * - declimal second * @return ms @@ -98,7 +106,7 @@ public static long toMS(String decimalSeconds) { /** * Fetch list of filenames, full path - * + * * @param dirname * location of submission files * @param runId @@ -123,4 +131,88 @@ public static List fetchRunFileNames(String dirname, String runId) { return list; } + /** + * Get files from a zipfile's base64 encoded string data + * + * @param base64Data String comprising a zip file encoded as base64 + * @return list of IFiles extracted from the input bytes + * @throws IllegalArgumentException from the base64 decoder on a data error + */ + public static List getIFiles(String base64Data) { + + // use the decoder to both check the validity of, and to store, the byte data. + // this will throw an IllegalArgumentException if the data is basd + return(getIFiles(Base64.getDecoder().decode(base64Data))); + } + + /** + * Get files from a zipfile's bytes. + * + * @param bytes bytes comprising a zip file. + * @return list of IFiles extracted from the input bytes + */ + public static List getIFiles(byte[] bytes) { + + List files = new ArrayList(); + + ZipInputStream zipStream = null; + + try { + zipStream = new ZipInputStream(new ByteArrayInputStream(bytes)); + ZipEntry entry = null; + /** + * Read each zip entry, add IFile. + */ + while ((entry = zipStream.getNextEntry()) != null) { + + String entryName = entry.getName(); + +// ByteOutputStream byteOutputStream = new ByteOutputStream(); + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + + byte[] buffer = new byte[8096]; + int bytesRead = 0; + while ((bytesRead = zipStream.read(buffer)) != -1) + { + byteOutputStream.write(buffer, 0, bytesRead); + } + +// String base64Data = getBase64Data(byteOutputStream.getBytes()); + String base64Data = getBase64Data(byteOutputStream.toByteArray()); + IFile iFile = new IFileImpl(entryName, base64Data); + files.add(iFile); + + byteOutputStream.close(); + + zipStream.closeEntry(); + } + zipStream.close(); + + } catch (Exception e) { + if (zipStream != null){ + try { + zipStream.close(); + } catch (Exception ze) { + ; // problem closing stream, ignore. + } + } + throw new RuntimeException(e); + } + + return files; + + } + + /** + * Encode bytes into BASE64. + * @param data + * @return + */ + public static String getBase64Data( byte [] bytes) { + // TODO REFACTOR move to FileUtilities + Base64.Encoder encoder = Base64.getEncoder(); + String base64String = encoder.encodeToString(bytes); + return base64String; + } + } diff --git a/src/edu/csus/ecs/pc2/shadow/RemoteContestAPIAdapter.java b/src/edu/csus/ecs/pc2/shadow/RemoteContestAPIAdapter.java index de7a46e00..0c59b9c4f 100644 --- a/src/edu/csus/ecs/pc2/shadow/RemoteContestAPIAdapter.java +++ b/src/edu/csus/ecs/pc2/shadow/RemoteContestAPIAdapter.java @@ -1,8 +1,7 @@ -// Copyright (C) 1989-2022 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2025 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.shadow; import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -10,29 +9,25 @@ import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; +import edu.csus.ecs.pc2.convert.EventFeedUtilities; import edu.csus.ecs.pc2.core.StringUtilities; import edu.csus.ecs.pc2.core.model.IFile; -import edu.csus.ecs.pc2.core.model.IFileImpl; import edu.csus.ecs.pc2.shadow.AbstractRemoteConfigurationObject.REMOTE_CONFIGURATION_ELEMENT; import edu.csus.ecs.pc2.util.HTTPSSecurity; public class RemoteContestAPIAdapter implements IRemoteContestAPIAdapter { - + URL remoteURL; String login; String password; - + /** * Constructs a RemoteRunMonitor with the specified values. - * + * * @param remoteURL the URL to the remote CCS * @param login the login (account) on the remote CCS * @param password the password to the remote CCS account @@ -56,13 +51,13 @@ public boolean testConnection() { } } catch (Exception e) { return false; // ignore exception, return false - } - - //if we get here, either the createConnection() returned null or else we got an exception + } + + //if we get here, either the createConnection() returned null or else we got an exception // which fell through the catch block return false; } - + protected URL getChildURL(String path) { if (path == null || path.isEmpty()) @@ -91,12 +86,12 @@ protected URL getChildURL(String path) { } } - + protected HttpURLConnection createConnection(String path) throws IOException { return createConnection(getChildURL(path)); } - - + + protected HttpURLConnection createConnection(URL url2) throws IOException { try { HttpURLConnection conn = HTTPSSecurity.createConnection(url2, login, password); @@ -108,7 +103,7 @@ protected HttpURLConnection createConnection(URL url2) throws IOException { throw new IOException("Connection error", e); } } - + //This method can be removed private InputStream connect(String path) throws IOException { try { @@ -133,10 +128,10 @@ else if (status == HttpURLConnection.HTTP_BAD_REQUEST) throw new IOException("Connection error", e); } } - + /** * Open input stream for event feed. - * + * * @param remoteURL2 * @param user * @param password @@ -151,7 +146,7 @@ private InputStream getHTTPInputStream(URL remoteURL2, String user, String passw @Override public RemoteContestConfiguration getRemoteContestConfiguration() { - + Map> remoteConfigMap = new HashMap>(); // TODO TODAY implement me - add mock data into RemoteContestConfiguration @@ -162,7 +157,7 @@ public RemoteContestConfiguration getRemoteContestConfiguration() { @Override public String getRemoteJSON(String endpoint) { - + String url = remoteURL.toString() + endpoint; try { HttpURLConnection conn = createConnection(url); @@ -170,11 +165,11 @@ public String getRemoteJSON(String endpoint) { } catch (IOException e) { throw new RuntimeException(e); } - + } private String toString(InputStream inputStream) throws IOException { - + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int result = bufferedInputStream.read(); @@ -184,9 +179,9 @@ private String toString(InputStream inputStream) throws IOException { } return byteArrayOutputStream.toString(); } - + private byte[] toByteArray(InputStream inputStream) throws IOException { - + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int result = bufferedInputStream.read(); @@ -205,16 +200,16 @@ public InputStream getRemoteEventFeedInputStream() { // passing null here will start the feed from the beginning return(getRemoteEventFeedInputStream(null)); } - + @Override /** * {@inheritDoc} */ public InputStream getRemoteEventFeedInputStream(String token) { - + String eventFeedURLString = remoteURL.toString(); eventFeedURLString = appendIfMissing(eventFeedURLString, "/") +"event-feed"; - + // Add on optional starting point token if(token != null && !token.isEmpty()) { eventFeedURLString += "?since_token=" + token; @@ -244,31 +239,31 @@ private String appendIfMissing(String s, String appendString) { * "/submissions//files" and then invoking the method * {@link #getRemoteSubmissionFiles(URL)} with that URL, returning the result * of that method call. - * + * * Note that if the "Primary CCS URL" ends with a "/" character then this method * avoids adding a duplicate "/" when concatenating the "submissions" endpoint to the URL; * see https://github.com/pc2ccs/pc2v9/issues/528. - * + * * @param submissionID a String representation of the submission ID from the remote system * @return the result of calling {@link #getRemoteSubmissionFiles(URL)} with the constructed URL, * or null if an exception is thrown during URL construction */ @Override public List getRemoteSubmissionFiles(String submissionID) { - + //define the CLICS endpoint for fetching the files associated with a submission String endpoint = "/submissions/" + submissionID + "/files"; - + //get the configured Primary CCS URL String urlString = remoteURL.toString(); //ensure the URL doesn't end with "/" (because we're going to add a "/" as part of the "endpoint") if (urlString.endsWith("/")){ urlString = StringUtilities.removeLastChar(urlString); } - + //build the full URL to the submissions/files endpoint urlString = urlString + endpoint; - + URL url; try { url = new URL(urlString); @@ -283,10 +278,10 @@ public List getRemoteSubmissionFiles(String submissionID) { /** * Fetches submission files from the specified URL. * The URL is expected to reference an endpoint which returns a zip file - * containing the files comprising a contest submission. - * + * containing the files comprising a contest submission. + * * @param submissionFilesURL a URL where a zip file containing submitted files may be found - * @return a List of {@link IFile}s containing the contents of the submission files obtained + * @return a List of {@link IFile}s containing the contents of the submission files obtained * from the specified URL * @throws {@link RuntimeException} if an {@link IOException} occurs while connecting to the * remote system at the specified URL or while reading bytes from the input stream @@ -299,90 +294,17 @@ public List getRemoteSubmissionFiles(URL submissionFilesURL) { //make a connection to the specified URL HttpURLConnection conn = createConnection(submissionFilesURL); - + //get the bytes (comprising a zipfile) from the specified URL's input stream byte[] bytes = toByteArray(conn.getInputStream()); // Unzip the bytes/zipfile into individual IFiles - List files = getIFiles(bytes); + List files = EventFeedUtilities.getIFiles(bytes); return files; } catch (IOException e) { throw new RuntimeException(e); } } - - /** - * Get files from a zipfile's bytes. - * - * @param bytes bytes comprising a zip file. - * @return list of IFiles extracted from the input bytes - */ - private List getIFiles(byte[] bytes) { - - List files = new ArrayList(); - - ZipInputStream zipStream = null; - - try { - zipStream = new ZipInputStream(new ByteArrayInputStream(bytes)); - ZipEntry entry = null; - /** - * Read each zip entry, add IFile. - */ - while ((entry = zipStream.getNextEntry()) != null) { - - String entryName = entry.getName(); - -// ByteOutputStream byteOutputStream = new ByteOutputStream(); - ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); - - byte[] buffer = new byte[8096]; - int bytesRead = 0; - while ((bytesRead = zipStream.read(buffer)) != -1) - { - byteOutputStream.write(buffer, 0, bytesRead); - } - -// String base64Data = getBase64Data(byteOutputStream.getBytes()); - String base64Data = getBase64Data(byteOutputStream.toByteArray()); - IFile iFile = new IFileImpl(entryName, base64Data); - files.add(iFile); - - byteOutputStream.close(); - - zipStream.closeEntry(); - } - zipStream.close(); - - } catch (Exception e) { - if (zipStream != null){ - try { - zipStream.close(); - } catch (Exception ze) { - ; // problem closing stream, ignore. - } - } - throw new RuntimeException(e); - } - - return files; - - } - - /** - * Encode bytes into BASE64. - * @param data - * @return - */ - public String getBase64Data( byte [] bytes) { - // TODO REFACTOR move to FileUtilities - Base64.Encoder encoder = Base64.getEncoder(); - String base64String = encoder.encodeToString(bytes); - return base64String; - } - - - }