/*
* Copyright 2003-2010 Tufts University 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://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
/**
* @author akumar03
* @version $Revision: 1.24 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $
*/
package tufts.vue;
//import java.io.BufferedOutputStream;
import java.io.BufferedReader;
//import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
//import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.rmi.RemoteException;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;
import javax.activation.MimetypesFileTypeMap;
import javax.swing.JOptionPane;
import javax.xml.namespace.QName;
import javax.xml.rpc.ServiceException;
import org.apache.axis.client.Call;
import org.apache.axis.client.Service;
import org.apache.axis.encoding.*;
import org.apache.log4j.Logger;
import org.jdom.*;
import edu.tufts.vue.dsm.DataSource;
public class SakaiPublisher {
/**
* All references to _local resources have URLs with a "file" prefix
*/
public static final String FILE_PREFIX = "file://";
public static final String DEFAULT_MIME_TYPE = "application/vue";
/**
* Maps published to Sakai are stored in folders that reflect their
* filename. The filename is transformed by replacing the ".vue" suffix
* with " vue map".
*/
public static final String VUE_MAP_FOLDER_SUFFIX = " vue map";
private static final Logger Log = Logger.getLogger(SakaiPublisher.class);
// Text saved as description of VUE resource saved to Sakai
private static final String RESOURCE_DESC = "VUE resource";
// Text saved as description of VUE map saved to Sakai if map has no "notes" content
private static final String MAP_DESC = "VUE map";
/** uploadMap
*
* @param dataSource DataSource object for Sakai LMS
* @param collectionId Workspace in Sakai to store map
* @param map VUE map to store in Sakai
* @param overwrite TODO
* @param publisher
*/
public static void uploadMap( DataSource dataSource, Object collectionId, LWMap map, int overwrite)
throws Exception
{
Properties dsConfig = dataSource.getConfiguration();
String sessionId = getSessionId(dsConfig);
String hostUrl = getHostUrl( dsConfig );
File savedMapFile = saveMapToFile( map );
String resourceName = savedMapFile.getName();
if( overwrite == JOptionPane.YES_OPTION)
{
}
// create folder for map and resources
String folderName = createFolder( sessionId, hostUrl, collectionId.toString(), makeSakaiFolderFromVueMap( resourceName ) );
if( savedMapFile.exists() ) {
hostUrl = getHostUrl( dsConfig );
// put map in a folder of the same name
uploadObjectToRepository(
hostUrl,
sessionId,
resourceName,
folderName,
savedMapFile,
map.hasNotes() ? map.getNotes() : MAP_DESC, false );
}
}
/** uploadMapAll
* Iterate over map, storing local resources in repository, rewriting the
* references in the map so they point to the remote location. Then store
* revised map in repository, leaving map in memory unchanged. (!?)
*
* Based on method of the same name in tufts.vue.FedoraPublisher.java.
*
* @param dataSource DataSource object for Sakai LMS
* @param collectionId Workspace in Sakai to store map
* @param map VUE map to store in Sakai
* @param overwrite TODO
*/
public static void uploadMapAll(DataSource dataSource, Object collectionId, LWMap map, int overwrite)
throws CloneNotSupportedException, IOException, ServiceException
{
Properties dsConfig = dataSource.getConfiguration();
String hostUrl = getHostUrl( dsConfig );
String sessionId = getSessionId( dsConfig );
File savedMapFile = saveMapToFile( map );
String resourceName = savedMapFile.getName();
// if there isn't a locally-saved map, then abort.
if( !savedMapFile.exists() ) {
throw new IOException();
}
// create folder for map and resources
String folderName = createFolder( sessionId, hostUrl, collectionId.toString(), makeSakaiFolderFromVueMap( resourceName ) );
/* I had this clever idea: Why not work with a clone instead of the
* real map, then if there was a problem I could roll back my changes
* and leave the real map unmodified. Anoop claims this works for him
* (see FedoraPublisher.java) but I didn't have the same luck. pdw 19-nov-07
*/
//LWMap cloneMap = (LWMap)map.clone();
//cloneMap.setLabel(map.getLabel());
String mapLabel = map.getLabel();
File origFile = map.getFile();
File tempFile = new File(VueUtil.getDefaultUserFolder()+File.separator+origFile.getName());
tempFile.deleteOnExit();
tufts.vue.action.ActionUtil.marshallMap(tempFile,map);
LWMap cloneMap = tufts.vue.action.OpenAction.loadMap(tempFile.getAbsolutePath());
//Iterator<LWComponent> i = map.getAllDescendents(LWComponent.ChildKind.ANY).iterator();
Iterator<LWComponent> i = cloneMap.getAllDescendents(LWComponent.ChildKind.PROPER).iterator();
while(i.hasNext())
{
LWComponent component = (LWComponent) i.next();
Log.debug("Component:" + component +" has resource:" + component.hasResource());
if(component.hasResource()
&& (component instanceof LWNode || component instanceof LWLink)
&& (component.getResource() instanceof URLResource))
{
URLResource resource = (URLResource) component.getResource();
System.out.println("Component:" + component
+ "file:" + resource.getSpec()
+ " has file:"+resource.getSpec().startsWith(FILE_PREFIX));
if(resource.isLocalFile()) {
File file = new File(resource.getSpec().replace(FILE_PREFIX,""));
System.out.println("LWComponent:" + component.getLabel()
+ " Resource: "+resource.getSpec()
+ " File:" + file + " exists:" + file.exists()
+ " MimeType" + new MimetypesFileTypeMap().getContentType(file));
// Maps created on another computer could contain a reference to a local file
// that doesn't exist on this user's computer. Don't process these.
if( !file.exists() ) {
continue;
}
uploadObjectToRepository(
getHostUrl( dsConfig ),
sessionId,
file.getName(),
folderName,
file,
RESOURCE_DESC, true );
//Replace the link for resource in the map
//TODO The following call to setProperty() clears the "File" property.
// this is necessary because currently setSpec() doesn't reset the File
// property, leaving a resource with both an URL and File property. It
// shouldn't have both. - pdw 28-nov-07
resource.removeProperty( "File" );
String ingestUrl = hostUrl + "/access/content" + folderName + (new File(resource.getSpec().replace(FILE_PREFIX,""))).getName();
//System.out.println( ingestUrl );
// resource.setSpec(ingestUrl);
component.setResource(URLResource.create(ingestUrl));
}
}
}
//upload the map
/* TODO NOTE: The map that is uploaded has changed from the map that
* was saved locally earlier this method. The difference is that the
* resources that were local now point to Sakai.
*/
//File tmpFile = tufts.vue.action.ActionUtil.selectFile("Save Map", "vue");
//File tmpFile = File.createTempFile("~vue-", ".tmp", VueUtil.getDefaultUserFolder());
//tmpFile.deleteOnExit();
// tufts.vue.action.ActionUtil.marshallMap( tmpFile );
//tufts.vue.action.ActionUtil.marshallMap( savedMapFile );
tufts.vue.action.ActionUtil.marshallMap(tempFile,cloneMap);
uploadObjectToRepository(
hostUrl,
sessionId,
resourceName,
folderName,
cloneMap.getFile(), //tmpFile,
cloneMap.hasNotes() ? cloneMap.getNotes() : MAP_DESC, false );
tufts.vue.action.ActionUtil.marshallMap(origFile, map);
}
/**
* Add a resource to a given collection. The resource is passed encoded
* using Base64.
*
* @param hostUrl
* @param sessionId a valid sessionid
* @param resourceName a name of the resource to be added
* @param collectionId collectionId of the collection it is to be added to
* @param file local resource
* @param description of the resource to be added
* @param isBinary if true, content is encoded using Base64, if false content is assumed to be text.
* @throws MalformedURLException
* @throws RemoteException
* @throws ServiceException
*/
private static void uploadObjectToRepository( String hostUrl, String sessionId,
String resourceName, String collectionId, File file, String description, boolean isBinary)
throws IOException, MalformedURLException, RemoteException, ServiceException
{
String contentMime = null; // contentMime content string
String type = getMimeType(file);
String retVal;
String endpoint = hostUrl + "/sakai-axis/ContentHosting.jws";
Service service = new Service();
Call call = (Call) service.createCall();
call.setTargetEndpointAddress(new java.net.URL(endpoint));
call.setOperationName(new QName(hostUrl + "/", "createContentItem"));
if( isBinary ) {
contentMime = Base64.encode(getByteArrayFromFile(file));
} else {
contentMime = getStringFromFile(file);
}
// createContentItem returns either "Success" or "Failure"
retVal = (String) call.invoke(
new Object[] { sessionId, resourceName, collectionId,
contentMime,
description, type, isBinary });
}
/**
* @param file
* @return file contents as a String
*/
private static String getStringFromFile(File file)
throws IOException {
String s;
StringBuffer sb = new StringBuffer();
BufferedReader in = new BufferedReader( new FileReader(file));
while( (s = in.readLine()) != null ) {
sb.append(s);
sb.append("\n");
}
in.close();
return sb.toString();
/* I had wanted to use the same routine for uploading the map as for uploading
* resources, but this code truncated the map file. This code is based on the
* assumption that I can treat a plain text file (a .VUE map file) as a binary
* file. For some reason that isn't apparent to me, it doesn't work.
*/
/* int bufferSize = 1024 * 8;
ByteBuffer buff = ByteBuffer.allocate( bufferSize );
ByteArrayOutputStream outBuf = new ByteArrayOutputStream( bufferSize );
int numRead = 0;
try {
FileChannel fc = new FileInputStream(file).getChannel();
while( (numRead = fc.read( buff )) >= 0 ) {
buff.flip();
outBuf.write( inBuf.array() );
inBuf.clear();
}
outBuf.flush(); // API docs say this is not necessary for this object
outBuf.close(); // API docs say this is not necessary for this object
} catch( FileNotFoundException e ) {
e.printStackTrace();
} catch( IOException e ) {
e.printStackTrace();
}
return outBuf.toByteArray();
*/
}
/**
* @param file
* @throws FileNotFoundException
* @throws IOException
*/
private static byte[] getByteArrayFromFile(File file)
{
//TODO: Optimize buffer size for this operation
int bufferSize = 1024 * 8;
ByteBuffer inBuf = ByteBuffer.allocate( bufferSize );
ByteArrayOutputStream outBuf = new ByteArrayOutputStream( bufferSize );
// I don't think that buffering adds anything here, but if it did
// here are the lines to add. Change outBuf to bos in the return statement.
//ByteArrayOutputStream bos = new ByteArrayOutputStream( bufferSize );
//BufferedOutputStream outBuf = new BufferedOutputStream( bos );
int numRead = 0;
try {
FileChannel fc = new FileInputStream(file).getChannel();
while( (numRead = fc.read( inBuf )) >= 0 ) {
inBuf.flip();
outBuf.write( inBuf.array() );
inBuf.clear();
}
outBuf.flush();
outBuf.close();
} catch( FileNotFoundException e ) {
e.printStackTrace();
} catch( IOException e ) {
e.printStackTrace();
}
return outBuf.toByteArray();
}
/**
* @param configuration
* @return authenticated session id
*/
public static String getSessionId( Properties configuration )
{
String username = configuration.getProperty("sakaiUsername");
String password = configuration.getProperty("sakaiPassword");
String hostUrl = getHostUrl( configuration );
String sessionId = null;
try {
String endpoint = hostUrl + "/sakai-axis/SakaiLogin.jws";
Service service = new Service();
Call call = (Call) service.createCall();
call.setTargetEndpointAddress(new java.net.URL(endpoint));
call.setOperationName(new QName(hostUrl + "/", "login"));
sessionId = (String) call
.invoke(new Object[] { username, password });
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
} catch (ServiceException e) {
e.printStackTrace();
}
return sessionId;
}
/**
* @param configuration
* @param sessionId
* @return the server's id string. This is concatenated with the
* session id to create a JSESSIONID cookie. VUE uses the cookie to
* access content using HTTP requests.
*/
public static String getServerId( Properties configuration, String sessionId )
{
String hostUrl = getHostUrl( configuration );
String serverId = null;
try {
String endpoint = hostUrl + "/sakai-axis/SakaiServerUtil.jws";
Service service = new Service();
Call call = (Call) service.createCall();
call.setTargetEndpointAddress(new java.net.URL(endpoint));
call.setOperationName(new QName(hostUrl + "/", "getSakaiServerId"));
serverId = (String) call
.invoke(new Object[] { sessionId });
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
} catch (ServiceException e) {
e.printStackTrace();
}
return serverId;
}
/**
* @param ds DataSource representing the Sakai repository
* @return JSESSIONID cookie string
*/
public static String getCookieString( DataSource ds )
{
Properties dsConfig = ds.getConfiguration();
String sessionId = getSessionId(dsConfig);
String serverId = getServerId( dsConfig, sessionId);
return "JSESSION=" + sessionId + "." + serverId;
}
/**
* @param map
* @param publisher
* @return File object that contains marshalled content of map parameter
*/
private static File saveMapToFile( LWMap map)
{
File tmpFile = map.getFile();
// map exists on disk, but hasn't been changed in memory
if( (!map.isModified()) && (null != tmpFile) )
{
tmpFile = map.getFile();
}
// map has changed in memory. (may exist on disk, but may not.)
if( map.isModified() )
{
tmpFile = new File(VueUtil.getDefaultUserFolder()+File.separator+map.getFile().getName());
tmpFile.deleteOnExit();
// tmpFile = tufts.vue.action.ActionUtil.selectFile("Save Map", "vue");
tufts.vue.action.ActionUtil.marshallMap( tmpFile );
}
return tmpFile;
}
/**
* @param sessionId
* @param hostUrl
* @param collectionId
* @param folderName
* @return collectionId of newly created folder
*/
private static String createFolder (String sessionId, String hostUrl, String collectionId, String folderName ) {
String resString = null;
String resId = collectionId + folderName;
try {
String endpoint = hostUrl + "/sakai-axis/ContentHosting.jws";
Service service = new Service();
// Set up content info.
//String name = "testFolder-"+String.valueOf(System.currentTimeMillis());
String content = "Test folder: "+ folderName;
//String desc = "Test web service Folder creation.";
Log.debug ("Folder name: "+ folderName +", content data: "+ content);
// Create a folder on server.
Call call = (Call) service.createCall();
call.setTargetEndpointAddress (new java.net.URL(endpoint) );
call.setOperationName(new QName(hostUrl + "/", "createFolder"));
// String session, String collectionId, String name
resString = (String) call.invoke( new Object[] {sessionId, collectionId, folderName} );
System.out.println("Sent ContentHosting.createFolder(sessionId, collId, name), got '" + resString + "'");
}
catch (Exception e) {
System.err.println(e.toString());
}
return resId + "/";
}
/**
* @param configuration
* @return
*/
private static String getHostUrl( Properties configuration )
{
String hostUrl = configuration.getProperty( "sakaiHost" );
String port = configuration.getProperty( "sakaiPort" );
if (!hostUrl.startsWith("http://")) {
// add http if it is not present
hostUrl = "http://" + hostUrl;
}
if( port != null && port.length() > 0 )
{
hostUrl = hostUrl + ":" + port;
}
return hostUrl;
}
/** Given a Sakai collection id, return its children, if any. For example,
* for this collection hierarchy:
* Bedrock
* Fred
* BamBam
* Wilma
* Pebbles
* a call to getChildCollections("/Bedrock/") would return Fred and Wilma,
* but not BamBam or Pebbles.
* @param collectionId
* @return
*/
public static Vector<String> getChildCollectionIds( String collectionId )
{
return new Vector();
}
/** Create a a Sakai folder name by replacing the ".vue" suffix with the
* defined folder suffix defined in VUE_MAP_FOLDER_SUFFIX.
*
* @param fileName is the name of the VUE map file
* @return name of Sakai folder to publish map into
*/
public static String makeSakaiFolderFromVueMap( String fileName )
{
String sakaiFolder = fileName.replaceAll("\\.vue$", VUE_MAP_FOLDER_SUFFIX );
return sakaiFolder;
}
private static String getMimeType(File file) {
String mimeType = DEFAULT_MIME_TYPE;
if(file.getName().endsWith("vue")) {
mimeType = DEFAULT_MIME_TYPE;
} else if(file != null) {
mimeType = new MimetypesFileTypeMap().getContentType(file) ;
}
return mimeType;
}
}