All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sakaiproject.contentreview.turnitin.util.TurnitinAPIUtil Maven / Gradle / Ivy

There is a newer version: 23.3
Show newest version
/**
 * 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.azeckoski.reflectutils.transcoders.XMLTranscoder;

import org.w3c.dom.Document;

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;

/**
 * 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 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) {
			log.error(e.getMessage(), e);
		}
		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 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));
			log.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 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 parameters, String secretKey) {
		if (!parameters.containsKey("fid")) {
			throw new IllegalArgumentException("You must to include a fid in the parameters");
		}
		
		StringBuilder apiDebugSB = new StringBuilder();
		if (log.isDebugEnabled()) {
			apiDebugSB.append("Starting URL TII Construction:\n");
		}
		
		parameters.put("gmtime", getGMTime());
		
		List sortedkeys = new ArrayList();
		sortedkeys.addAll(parameters.keySet());

		String md5 = buildTurnitinMD5(parameters, secretKey, sortedkeys);
		
		StringBuilder sb = new StringBuilder();
		sb.append(apiURL);
		if (log.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 (log.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 (log.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 (log.isDebugEnabled()) {
			apiDebugSB.append("md5 = ");
			apiDebugSB.append(md5);
			apiDebugSB.append("\n");
			log.debug(apiDebugSB.toString());
		}
		
		return sb.toString();
	}

	public static Document callTurnitinReturnDocument(String apiURL, Map 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 (log.isDebugEnabled()) {
			log.debug(" Result from call: " + Xml.writeDocumentToString(document));
		}

		return document;
	}

	public static InputStream callTurnitinReturnInputStream(String apiURL, Map 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> ets = parameters.entrySet();
			Iterator> it = ets.iterator();
			while (it.hasNext()) {
				Entry entr = it.next();
				log.debug("Paramater entry: " + entr.getKey() + ": " + entr.getValue());
			}
		}
		
		List sortedkeys = new ArrayList();
		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 (log.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 (log.isDebugEnabled()) {
							apiDebugSB.append(sortedkeys.get(i));
							apiDebugSB.append(" = ContentHostingResource: ");
							apiDebugSB.append(resource.getId());
							apiDebugSB.append("\n");
						}
					}
					else {
						if (log.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 (log.isDebugEnabled()) {
					apiDebugSB.append("md5 = ");
					apiDebugSB.append(md5);
					apiDebugSB.append("\n");
					log.debug(apiDebugSB.toString());
				}
			}
			else {
				writeBytesToOutputStream(outStream, sortedkeys.get(0),"=",
						parameters.get(sortedkeys.get(0)).toString());
				if (log.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 (log.isDebugEnabled()) {
						apiDebugSB.append(sortedkeys.get(i));
						apiDebugSB.append(" = ");
						apiDebugSB.append(parameters.get(sortedkeys.get(i)).toString());
						apiDebugSB.append("\n");
					}
				}

				writeBytesToOutputStream(outStream, "&md5=", md5);
				if (log.isDebugEnabled()) {
					apiDebugSB.append("md5 = ");
					apiDebugSB.append(md5);
					log.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 parameters,
			String secretKey, List 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";
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy