Java tutorial
/** * Copyright (c) 2003 The Apereo Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://opensource.org/licenses/ecl2 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sakaiproject.contentreview.turnitin.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.Proxy; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.TimeZone; import java.util.Map.Entry; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.azeckoski.reflectutils.transcoders.XMLTranscoder; import org.sakaiproject.content.api.ContentResource; import org.sakaiproject.contentreview.exception.SubmissionException; import org.sakaiproject.contentreview.exception.TransientSubmissionException; import org.sakaiproject.exception.ServerOverloadException; import org.sakaiproject.util.Xml; import org.w3c.dom.Document; /** * This is a utility class for wrapping the physical https calls to the * Turn It In Service. * * @author sgithens * */ @Slf4j public class TurnitinAPIUtil { private static final Logger apiTraceLog = LoggerFactory .getLogger("org.sakaiproject.turnitin.util.TurnitinAPIUtil.apicalltrace"); private static String encodeSakaiTitles(String assignTitle) { String assignEnc = assignTitle; try { if (assignTitle.contains("&")) { //log.debug("replacing & in assingment title"); assignTitle = assignTitle.replace('&', 'n'); } assignEnc = assignTitle; log.debug("Assign title is " + assignEnc); } catch (Exception e) { e.printStackTrace(); } return assignEnc; } private String encodeMultipartParam(String name, String value, String boundary) { return "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n"; } private static HttpsURLConnection fetchConnection(String apiURL, int timeout, Proxy proxy) throws MalformedURLException, IOException, ProtocolException { HttpsURLConnection connection; URL hostURL = new URL(apiURL); if (proxy == null) { connection = (HttpsURLConnection) hostURL.openConnection(); } else { connection = (HttpsURLConnection) hostURL.openConnection(proxy); } // This actually turns into a POST since we are writing to the // resource body. ( You can see this in Webscarab or some other HTTP // interceptor. connection.setRequestMethod("GET"); connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); connection.setDoOutput(true); connection.setDoInput(true); return connection; } public static String getGMTime() { // calculate function2 data SimpleDateFormat dform = ((SimpleDateFormat) DateFormat.getDateInstance()); dform.applyPattern("yyyyMMddHH"); dform.setTimeZone(TimeZone.getTimeZone("GMT")); Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); String gmtime = dform.format(cal.getTime()); gmtime += Integer.toString(((int) Math.floor((double) cal.get(Calendar.MINUTE) / 10))); return gmtime; } @SuppressWarnings({ "unchecked" }) public static Map packMap(Map map, Object... vargs) { if (map == null) { map = new HashMap(); } if (vargs.length % 2 != 0) { throw new IllegalArgumentException("You need to supply an even number of vargs for the key-val pairs."); } for (int i = 0; i < vargs.length; i += 2) { map.put(vargs[i], vargs[i + 1]); } return map; } public static void writeBytesToOutputStream(OutputStream outStream, String... vargs) throws UnsupportedEncodingException, IOException { for (String next : vargs) { outStream.write(next.getBytes("UTF-8")); } } public static String getMD5(String md5_string) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(md5_string.getBytes()); // convert the binary md5 hash into hex String md5 = ""; byte[] b_arr = md.digest(); for (int i = 0; i < b_arr.length; i++) { // convert the high nibble byte b = b_arr[i]; b >>>= 4; b &= 0x0f; // this clears the top half of the byte md5 += Integer.toHexString(b); // convert the low nibble b = b_arr[i]; b &= 0x0F; md5 += Integer.toHexString(b); } return md5; } public static Map callTurnitinReturnMap(String apiURL, Map<String, Object> parameters, String secretKey, int timeout, Proxy proxy) throws TransientSubmissionException, SubmissionException { XMLTranscoder xmlt = new XMLTranscoder(); try (InputStream inputStream = callTurnitinReturnInputStream(apiURL, parameters, secretKey, timeout, proxy, false)) { Map togo = xmlt.decode(IOUtils.toString(inputStream)); apiTraceLog.debug("Turnitin Result Payload: " + togo); return togo; } catch (Exception t) { // Could be 'java.lang.IllegalArgumentException: xml cannot be null or empty' from IO errors throw new TransientSubmissionException("Cannot parse Turnitin response. Assuming call was unsuccessful", t); } } public static Document callTurnitinReturnDocument(String apiURL, Map<String, Object> parameters, String secretKey, int timeout, Proxy proxy) throws TransientSubmissionException, SubmissionException { return callTurnitinReturnDocument(apiURL, parameters, secretKey, timeout, proxy, false); } public static String buildTurnitinURL(String apiURL, Map<String, Object> parameters, String secretKey) { if (!parameters.containsKey("fid")) { throw new IllegalArgumentException("You must to include a fid in the parameters"); } StringBuilder apiDebugSB = new StringBuilder(); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append("Starting URL TII Construction:\n"); } parameters.put("gmtime", getGMTime()); List<String> sortedkeys = new ArrayList<String>(); sortedkeys.addAll(parameters.keySet()); String md5 = buildTurnitinMD5(parameters, secretKey, sortedkeys); StringBuilder sb = new StringBuilder(); sb.append(apiURL); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append("The TII Base URL is:\n"); apiDebugSB.append(apiURL); } sb.append(sortedkeys.get(0)); sb.append("="); sb.append(parameters.get(sortedkeys.get(0))); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append(sortedkeys.get(0)); apiDebugSB.append("="); apiDebugSB.append(parameters.get(sortedkeys.get(0))); apiDebugSB.append("\n"); } for (int i = 1; i < sortedkeys.size(); i++) { sb.append("&"); sb.append(sortedkeys.get(i)); sb.append("="); sb.append(parameters.get(sortedkeys.get(i))); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append(sortedkeys.get(i)); apiDebugSB.append(" = "); apiDebugSB.append(parameters.get(sortedkeys.get(i))); apiDebugSB.append("\n"); } } sb.append("&"); sb.append("md5="); sb.append(md5); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append("md5 = "); apiDebugSB.append(md5); apiDebugSB.append("\n"); apiTraceLog.debug(apiDebugSB.toString()); } return sb.toString(); } public static Document callTurnitinReturnDocument(String apiURL, Map<String, Object> parameters, String secretKey, int timeout, Proxy proxy, boolean isMultipart) throws TransientSubmissionException, SubmissionException { InputStream inputStream = callTurnitinReturnInputStream(apiURL, parameters, secretKey, timeout, proxy, isMultipart); BufferedReader in; in = new BufferedReader(new InputStreamReader(inputStream)); Document document = null; try { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder parser = documentBuilderFactory.newDocumentBuilder(); document = parser.parse(new org.xml.sax.InputSource(in)); } catch (ParserConfigurationException pce) { log.error("parser configuration error: " + pce.getMessage()); throw new TransientSubmissionException("Parser configuration error", pce); } catch (Exception t) { throw new TransientSubmissionException("Cannot parse Turnitin response. Assuming call was unsuccessful", t); } if (apiTraceLog.isDebugEnabled()) { apiTraceLog.debug(" Result from call: " + Xml.writeDocumentToString(document)); } return document; } public static InputStream callTurnitinReturnInputStream(String apiURL, Map<String, Object> parameters, String secretKey, int timeout, Proxy proxy, boolean isMultipart) throws TransientSubmissionException, SubmissionException { InputStream togo = null; StringBuilder apiDebugSB = new StringBuilder(); if (!parameters.containsKey("fid")) { throw new IllegalArgumentException("You must to include a fid in the parameters"); } //if (!parameters.containsKey("gmttime")) { parameters.put("gmtime", getGMTime()); //} /** * Some debug logging */ if (log.isDebugEnabled()) { Set<Entry<String, Object>> ets = parameters.entrySet(); Iterator<Entry<String, Object>> it = ets.iterator(); while (it.hasNext()) { Entry<String, Object> entr = it.next(); log.debug("Paramater entry: " + entr.getKey() + ": " + entr.getValue()); } } List<String> sortedkeys = new ArrayList<String>(); sortedkeys.addAll(parameters.keySet()); String md5 = buildTurnitinMD5(parameters, secretKey, sortedkeys); HttpsURLConnection connection; String boundary = ""; try { connection = fetchConnection(apiURL, timeout, proxy); connection.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); if (isMultipart) { Random rand = new Random(); //make up a boundary that should be unique boundary = Long.toString(rand.nextLong(), 26) + Long.toString(rand.nextLong(), 26) + Long.toString(rand.nextLong(), 26); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); } log.debug("HTTPS Connection made to Turnitin"); OutputStream outStream = connection.getOutputStream(); if (isMultipart) { if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append("Starting Multipart TII CALL:\n"); } for (int i = 0; i < sortedkeys.size(); i++) { if (parameters.get(sortedkeys.get(i)) instanceof ContentResource) { ContentResource resource = (ContentResource) parameters.get(sortedkeys.get(i)); outStream.write( ("--" + boundary + "\r\nContent-Disposition: form-data; name=\"pdata\"; filename=\"" + resource.getId() + "\"\r\n" + "Content-Type: " + resource.getContentType() + "\r\ncontent-transfer-encoding: binary" + "\r\n\r\n").getBytes()); //TODO this loads the doc into memory rather use the stream method byte[] content = resource.getContent(); if (content == null) { throw new SubmissionException("zero length submission!"); } outStream.write(content); outStream.write("\r\n".getBytes("UTF-8")); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append(sortedkeys.get(i)); apiDebugSB.append(" = ContentHostingResource: "); apiDebugSB.append(resource.getId()); apiDebugSB.append("\n"); } } else { if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append(sortedkeys.get(i)); apiDebugSB.append(" = "); apiDebugSB.append(parameters.get(sortedkeys.get(i)).toString()); apiDebugSB.append("\n"); } outStream.write(encodeParam(sortedkeys.get(i), parameters.get(sortedkeys.get(i)).toString(), boundary).getBytes()); } } outStream.write(encodeParam("md5", md5, boundary).getBytes()); outStream.write(("--" + boundary + "--").getBytes()); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append("md5 = "); apiDebugSB.append(md5); apiDebugSB.append("\n"); apiTraceLog.debug(apiDebugSB.toString()); } } else { writeBytesToOutputStream(outStream, sortedkeys.get(0), "=", parameters.get(sortedkeys.get(0)).toString()); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append("Starting TII CALL:\n"); apiDebugSB.append(sortedkeys.get(0)); apiDebugSB.append(" = "); apiDebugSB.append(parameters.get(sortedkeys.get(0)).toString()); apiDebugSB.append("\n"); } for (int i = 1; i < sortedkeys.size(); i++) { writeBytesToOutputStream(outStream, "&", sortedkeys.get(i), "=", parameters.get(sortedkeys.get(i)).toString()); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append(sortedkeys.get(i)); apiDebugSB.append(" = "); apiDebugSB.append(parameters.get(sortedkeys.get(i)).toString()); apiDebugSB.append("\n"); } } writeBytesToOutputStream(outStream, "&md5=", md5); if (apiTraceLog.isDebugEnabled()) { apiDebugSB.append("md5 = "); apiDebugSB.append(md5); apiTraceLog.debug(apiDebugSB.toString()); } } outStream.close(); togo = connection.getInputStream(); } catch (IOException t) { log.error("IOException making turnitin call.", t); throw new TransientSubmissionException("IOException making turnitin call.", t); } catch (ServerOverloadException t) { throw new TransientSubmissionException("Unable to submit the content data from ContentHosting", t); } return togo; } private static String buildTurnitinMD5(Map<String, Object> parameters, String secretKey, List<String> sortedkeys) { TIIFID fid = TIIFID.getFid(Integer.parseInt((String) parameters.get("fid"))); Collections.sort(sortedkeys); StringBuilder md5sb = new StringBuilder(); for (int i = 0; i < sortedkeys.size(); i++) { if (fid.includeParamInMD5(sortedkeys.get(i))) { md5sb.append(parameters.get(sortedkeys.get(i))); } } md5sb.append(secretKey); String md5; try { md5 = getMD5(md5sb.toString()); } catch (NoSuchAlgorithmException t) { log.warn("MD5 error creating class on turnitin"); throw new RuntimeException("Cannot generate MD5 hash for Turnitin API call", t); } return md5; } private static String encodeParam(String name, String value, String boundary) { return "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n"; } }