/** * Copyright (c) 2008-2012 The Sakai 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://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. */ package org.sakaiproject.profile2.conversion; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import lombok.Data; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.sakaiproject.api.common.edu.person.SakaiPerson; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.profile2.dao.ProfileDao; import org.sakaiproject.profile2.exception.ProfileNotDefinedException; import org.sakaiproject.profile2.hbm.model.ProfileImageExternal; import org.sakaiproject.profile2.hbm.model.ProfileImageUploaded; import org.sakaiproject.profile2.logic.ProfileImageLogic; import org.sakaiproject.profile2.logic.SakaiProxy; import org.sakaiproject.profile2.model.ImportableUserProfile; import org.sakaiproject.profile2.model.MimeTypeByteArray; import org.sakaiproject.profile2.model.UserProfile; import org.sakaiproject.profile2.util.ProfileConstants; import org.sakaiproject.profile2.util.ProfileUtils; import au.com.bytecode.opencsv.CSVReader; import au.com.bytecode.opencsv.bean.CsvToBean; import au.com.bytecode.opencsv.bean.HeaderColumnNameTranslateMappingStrategy; /** * Handles the conversion and import of profiles and images. This is not part of the public API. * * @author Steve Swinsburg (steve.swinsburg@gmail.com) * */ public class ProfileConverter { private static final Logger log = Logger.getLogger(ProfileConverter.class); @Setter private SakaiProxy sakaiProxy; @Setter private ProfileDao dao; @Setter private SecurityService securityService; @Setter private ProfileImageLogic imageLogic; ConvertedImage ci = null; private final static String DEFAULT_FILE_NAME = "Profile Image"; private final static String DEFAULT_MIME_TYPE = "image/jpeg"; public void init() { log.info("Profile2: ==============================="); log.info("Profile2: Conversion utility starting up."); log.info("Profile2: ==============================="); } /** * Convert profile images */ public void convertProfileImages() { //get list of users List<String> allUsers = new ArrayList<String>(dao.getAllSakaiPersonIds()); if(allUsers.isEmpty()){ log.warn("Profile2 image converter: No SakaiPersons to process. Nothing to do!"); return; } //for each, do they have a profile image record. if so, skip (perhaps null the SakaiPerson JPEG_PHOTO bytes?) for(Iterator<String> i = allUsers.iterator(); i.hasNext();) { String userUuid = (String)i.next(); //get image record from dao directly, we don't need privacy/prefs here ProfileImageUploaded uploadedProfileImage = dao.getCurrentProfileImageRecord(userUuid); ci = new ConvertedImage(); ci.setUserUuid(userUuid); //if no record, we need to run all conversions if(uploadedProfileImage == null) { //main convertSakaiPersonImage(); if(StringUtils.isNotBlank(ci.getMainResourceId())) { //thumbnail generateAndPersistThumbnail(); //avatar generateAndPersistAvatar(); } } else { //get any existing values and set into object so we know if we need to generate or save anything ci.setMainResourceId(uploadedProfileImage.getMainResource()); ci.setThumbnailResourceId(uploadedProfileImage.getThumbnailResource()); ci.setAvatarResourceId(uploadedProfileImage.getAvatarResource()); //get the existing profile image MimeTypeByteArray mtba = sakaiProxy.getResource(ci.getMainResourceId()); ci.setImage(mtba.getBytes()); ci.setMimeType(mtba.getMimeType()); //if we need thumb or avatar, create as necessary if(ci.needsThumb()){ generateAndPersistThumbnail(); } if(ci.needsAvatar()){ generateAndPersistAvatar(); } } //save image resource IDs if(ci.isNeedsSaving()){ ProfileImageUploaded convertedProfileImage = new ProfileImageUploaded(userUuid, ci.getMainResourceId(), ci.getThumbnailResourceId(), ci.getAvatarResourceId(), true); if(dao.addNewProfileImage(convertedProfileImage)){ log.info("Profile2 image converter: Binary image converted and saved for " + userUuid); } else { log.warn("Profile2 image converter: Binary image conversion failed for " + userUuid); } } // // Process external image urls // //process any image URLs, if they don't already have a valid record. ProfileImageExternal externalProfileImage = dao.getExternalImageRecordForUser(userUuid); if(externalProfileImage != null) { log.info("Profile2 image converter: ProfileImageExternal record exists for " + userUuid + ". Nothing to do here, skipping..."); } else { log.info("Profile2 image converter: No existing ProfileImageExternal record for " + userUuid + ". Processing..."); String url = sakaiProxy.getSakaiPersonImageUrl(userUuid); //if none, nothing to do if(StringUtils.isBlank(url)) { log.info("Profile2 image converter: No url image to convert for " + userUuid + ". Skipping..."); } else { externalProfileImage = new ProfileImageExternal(userUuid, url, null, null); if(dao.saveExternalImage(externalProfileImage)) { log.info("Profile2 image converter: Url image converted and saved for " + userUuid); } else { log.warn("Profile2 image converter: Url image conversion failed for " + userUuid); } } } log.info("Profile2 image converter: Finished converting user profile for: " + userUuid); //go to next user } return; } /** * This imports URL profile images into upload profile images. */ public void importProfileImages() { //get list of users List<String> allUsers = new ArrayList<String>(dao.getAllSakaiPersonIds()); if(allUsers.isEmpty()){ log.warn("Profile2 image converter: No SakaiPersons to process. Nothing to do!"); return; } //for each, do they have a profile image record. if so, skip (perhaps null the SakaiPerson JPEG_PHOTO bytes?) for(Iterator<String> i = allUsers.iterator(); i.hasNext();) { String userUuid = i.next(); //get image record from dao directly, we don't need privacy/prefs here ProfileImageUploaded uploadedProfileImage = dao.getCurrentProfileImageRecord(userUuid); ci = new ConvertedImage(); ci.setUserUuid(userUuid); //if no record, we need to run all conversions if(uploadedProfileImage == null) { //main ProfileImageExternal externalProfileImage = dao.getExternalImageRecordForUser(userUuid); if (externalProfileImage == null) { log.info("No existing external profile images for "+ userUuid); } else { String mainUrl = externalProfileImage.getMainUrl(); if (StringUtils.isNotBlank(mainUrl)) { retrieveMainImage(userUuid, mainUrl); } else { log.info("No URL set for "+ userUuid); } } if(StringUtils.isNotBlank(ci.getMainResourceId())) { //thumbnail generateAndPersistThumbnail(); //avatar generateAndPersistAvatar(); } } //save image resource IDs if(ci.isNeedsSaving()){ ProfileImageUploaded convertedProfileImage = new ProfileImageUploaded(userUuid, ci.getMainResourceId(), ci.getThumbnailResourceId(), ci.getAvatarResourceId(), true); if(dao.addNewProfileImage(convertedProfileImage)){ log.info("Profile2 image converter: Binary image converted and saved for " + userUuid); } else { log.warn("Profile2 image converter: Binary image conversion failed for " + userUuid); } } } } private void retrieveMainImage(String userUuid, String mainUrl) { InputStream inputStream = null; try { URL url = new URL(mainUrl); HttpURLConnection openConnection = (HttpURLConnection) url.openConnection(); openConnection.setReadTimeout(5000); openConnection.setConnectTimeout(5000); openConnection.setInstanceFollowRedirects(true); openConnection.connect(); int responseCode = openConnection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { String mimeType = openConnection.getContentType(); inputStream = openConnection.getInputStream(); // Convert the image. byte[] imageMain = ProfileUtils.scaleImage(inputStream, ProfileConstants.MAX_IMAGE_XY, mimeType); //create resource ID String mainResourceId = sakaiProxy.getProfileImageResourcePath(userUuid, ProfileConstants.PROFILE_IMAGE_MAIN); log.info("Profile2 image converter: mainResourceId: " + mainResourceId); //save, if error, log and return. if (!sakaiProxy.saveFile(mainResourceId, userUuid, DEFAULT_FILE_NAME, mimeType, imageMain)) { log.error("Profile2 image importer: Saving main profile image failed."); } else { ci.setImage(imageMain); ci.setMimeType(mimeType); ci.setFileName(DEFAULT_FILE_NAME); ci.setMainResourceId(mainResourceId); ci.setNeedsSaving(true); } } else { log.warn("Failed to get good response for user "+ userUuid+ " for "+ mainUrl+ " got "+ responseCode); } } catch (MalformedURLException e) { log.info ("Invalid URL for user: "+ userUuid+ " of: "+ mainUrl); } catch (IOException e) { log.warn("Failed to download image for: "+ userUuid+ " from: "+ mainUrl+ " error of: "+ e.getMessage()); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException ioe) { log.info("Failed to close input stream for request to: "+ mainUrl); } } } } /** * Import profiles from the given CSV file * * <p>The CSV file may contain any of the following headings, in any order: * * <ul> * <li>eid</li> * <li>nickname</li> * <li>position</li> * <li>department</li> * <li>school</li> * <li>room</li> * <li>web site</li> * <li>work phone</li> * <li>home phone</li> * <li>mobile phone</li> * <li>fax</li> * <li>books</li> * <li>tv</li> * <li>movies</li> * <li>quotes</li> * <li>summary</li> * <li>course</li> * <li>subjects</li> * <li>staff profile</li> * <li>uni profile url</li> * <li>academic profile url</li> * <li>publications</li> * <li>official image url</li> * </ul> * * <p>Column headings must match EXACTLY the list above. They do not need to be in the same order, or even all present. * * <p>Fields must be comma separated and each field surrounded with double quotes. There must be no spaces between fields. * * <p>Only users that do not currently have a profile will be imported. * * @param path path to CSV file on the server */ public void importProfiles(String path) { if(StringUtils.isBlank(path)) { log.warn("Profile2 importer: invalid path to CSV file. Aborting."); return; } HeaderColumnNameTranslateMappingStrategy<ImportableUserProfile> strat = new HeaderColumnNameTranslateMappingStrategy<ImportableUserProfile>(); strat.setType(ImportableUserProfile.class); //map the column headers to the field names in the UserProfile class //this mapping is not exhaustive and can be added to at any time since we are mapping //on column name not position Map<String, String> map = new HashMap<String, String>(); map.put("eid", "eid"); map.put("nickname", "nickname"); map.put("position", "position"); map.put("department", "department"); map.put("school", "school"); map.put("room", "room"); map.put("web site", "homepage"); map.put("work phone", "workphone"); map.put("home phone", "homephone"); map.put("mobile phone", "mobilephone"); map.put("fax", "facsimile"); map.put("books", "favouriteBooks"); map.put("tv", "favouriteTvShows"); map.put("movies", "favouriteMovies"); map.put("quotes", "favouriteQuotes"); map.put("summary", "personalSummary"); map.put("course", "course"); map.put("subjects", "subjects"); map.put("staff profile", "staffProfile"); map.put("uni profile url", "universityProfileUrl"); map.put("academic profile url", "academicProfileUrl"); map.put("publications", "publications"); map.put("official image url", "officialImageUrl"); strat.setColumnMapping(map); CsvToBean<ImportableUserProfile> csv = new CsvToBean<ImportableUserProfile>(); List<ImportableUserProfile> list = new ArrayList<ImportableUserProfile>(); try { list = csv.parse(strat, new CSVReader(new FileReader(path))); } catch (FileNotFoundException fnfe) { log.error("Profile2 importer: Couldn't find file: " + fnfe.getClass() + " : " + fnfe.getMessage()); } //setup a security advisor so we can save profiles SecurityAdvisor securityAdvisor = new SecurityAdvisor(){ public SecurityAdvice isAllowed(String userId, String function, String reference){ return SecurityAdvice.ALLOWED; } }; enableSecurityAdvisor(securityAdvisor); //process each for(ImportableUserProfile profile: list) { log.info("Processing user: " + profile.getEid()); //get uuid String uuid = sakaiProxy.getUserIdForEid(profile.getEid()); if(StringUtils.isBlank(uuid)) { log.error("Invalid user: " + profile.getEid() + ". Skipping..."); continue; } profile.setUserUuid(uuid); //check if user already has a profile. Skip if so. if(hasPersistentProfile(uuid)) { log.warn("User: " + profile.getEid() + " already has a profile. Skipping..."); continue; } //persist user profile try { SakaiPerson sp = transformUserProfileToSakaiPerson(profile); if(sp == null){ //already logged continue; } if(sakaiProxy.updateSakaiPerson(sp)) { log.info("Profile saved for user: " + profile.getEid()); } else { log.error("Couldn't save profile for user: " + profile.getEid()); continue; } } catch (ProfileNotDefinedException pnde) { //already logged continue; } //add/update official image, if supplied in the CSV if(StringUtils.isNotBlank(profile.getOfficialImageUrl())) { if(imageLogic.saveOfficialImageUrl(uuid, profile.getOfficialImageUrl())) { log.info("Official image saved for user: " + profile.getEid()); } else { log.error("Couldn't save official image for user: " + profile.getEid()); } } } disableSecurityAdvisor(securityAdvisor); } /** * Does the given user already have a <b>persistent</b> user profile? * * @param userUuid uuid of the user * @return */ private boolean hasPersistentProfile(String userUuid) { SakaiPerson sp = sakaiProxy.getSakaiPerson(userUuid); if(sp != null){ return true; } return false; } /** * Convenience method to map a UserProfile object onto a SakaiPerson object for persisting * * @param up input UserProfile * @return returns a SakaiPerson representation of the UserProfile object which can be persisted */ private SakaiPerson transformUserProfileToSakaiPerson(UserProfile up) { log.info("Transforming: " + up.toString()); String userUuid = up.getUserUuid(); if(StringUtils.isBlank(userUuid)) { log.error("Profile was invalid (missing uuid), cannot transform."); return null; } //get SakaiPerson SakaiPerson sakaiPerson = sakaiProxy.getSakaiPerson(userUuid); //if null, create one if(sakaiPerson == null) { sakaiPerson = sakaiProxy.createSakaiPerson(userUuid); //if its still null, throw exception if(sakaiPerson == null) { throw new ProfileNotDefinedException("Couldn't create a SakaiPerson for " + userUuid); } } //map fields from UserProfile to SakaiPerson //basic info sakaiPerson.setNickname(up.getNickname()); sakaiPerson.setDateOfBirth(up.getDateOfBirth()); //contact info sakaiPerson.setLabeledURI(up.getHomepage()); sakaiPerson.setTelephoneNumber(up.getWorkphone()); sakaiPerson.setHomePhone(up.getHomephone()); sakaiPerson.setMobile(up.getMobilephone()); sakaiPerson.setFacsimileTelephoneNumber(up.getFacsimile()); //staff info sakaiPerson.setOrganizationalUnit(up.getDepartment()); sakaiPerson.setTitle(up.getPosition()); sakaiPerson.setCampus(up.getSchool()); sakaiPerson.setRoomNumber(up.getRoom()); sakaiPerson.setStaffProfile(up.getStaffProfile()); sakaiPerson.setUniversityProfileUrl(up.getUniversityProfileUrl()); sakaiPerson.setAcademicProfileUrl(up.getAcademicProfileUrl()); sakaiPerson.setPublications(up.getPublications()); // student info sakaiPerson.setEducationCourse(up.getCourse()); sakaiPerson.setEducationSubjects(up.getSubjects()); //personal info sakaiPerson.setFavouriteBooks(up.getFavouriteBooks()); sakaiPerson.setFavouriteTvShows(up.getFavouriteTvShows()); sakaiPerson.setFavouriteMovies(up.getFavouriteMovies()); sakaiPerson.setFavouriteQuotes(up.getFavouriteQuotes()); sakaiPerson.setNotes(up.getPersonalSummary()); return sakaiPerson; } /** * Add the supplied security advisor to the stack for this transaction */ private void enableSecurityAdvisor(SecurityAdvisor securityAdvisor) { securityService.pushAdvisor(securityAdvisor); } /** * Remove security advisor from the stack */ private void disableSecurityAdvisor(SecurityAdvisor advisor){ securityService.popAdvisor(advisor); } /** * Helper to convert an image stored in SakaiPerson into a main image * @return */ private void convertSakaiPersonImage(){ String userUuid = ci.getUserUuid(); //get photo from SakaiPerson byte[] image = sakaiProxy.getSakaiPersonJpegPhoto(userUuid); //if none, nothing to do if(image == null || image.length == 0) { log.info("Profile2 image converter: No image binary to convert for " + userUuid + ". Skipping user..."); } else { //scale the main image byte[] imageMain = ProfileUtils.scaleImage(image, ProfileConstants.MAX_IMAGE_XY, DEFAULT_MIME_TYPE); //create resource ID String mainResourceId = sakaiProxy.getProfileImageResourcePath(userUuid, ProfileConstants.PROFILE_IMAGE_MAIN); log.info("Profile2 image converter: mainResourceId: " + mainResourceId); //save, if error, log and return. if(!sakaiProxy.saveFile(mainResourceId, userUuid, DEFAULT_FILE_NAME, DEFAULT_MIME_TYPE, imageMain)) { log.error("Profile2 image converter: Saving main profile image failed."); } else { ci.setImage(imageMain); ci.setMimeType(DEFAULT_MIME_TYPE); ci.setFileName(DEFAULT_FILE_NAME); ci.setMainResourceId(mainResourceId); ci.setNeedsSaving(true); } } } /** * Helper to convert an image into a thumbnail * @return */ private void generateAndPersistThumbnail() { String userUuid = ci.getUserUuid(); byte[] imageThumbnail = ProfileUtils.scaleImage(ci.getImage(), ProfileConstants.MAX_THUMBNAIL_IMAGE_XY, ci.getMimeType()); //create resource ID String thumbnailResourceId = sakaiProxy.getProfileImageResourcePath(userUuid, ProfileConstants.PROFILE_IMAGE_THUMBNAIL); log.info("Profile2 image converter: thumbnailResourceId:" + thumbnailResourceId); //save, if error, log and return. if(!sakaiProxy.saveFile(thumbnailResourceId, userUuid, DEFAULT_FILE_NAME, ci.getMimeType(), imageThumbnail)) { log.warn("Profile2 image converter: Saving thumbnail profile image failed. Main image will be used instead."); } else { ci.setThumbnailResourceId(thumbnailResourceId); ci.setNeedsSaving(true); } } /** * Helper to convert an image into a thumbnail * @return */ private void generateAndPersistAvatar() { String userUuid = ci.getUserUuid(); byte[] imageAvatar = ProfileUtils.createAvatar(ci.getImage(), ci.getMimeType()); //create resource ID String avatarResourceId = sakaiProxy.getProfileImageResourcePath(userUuid, ProfileConstants.PROFILE_IMAGE_THUMBNAIL); log.info("Profile2 image converter: avatarResourceId:" + avatarResourceId); //save, if error, log and return. if(!sakaiProxy.saveFile(avatarResourceId, userUuid, DEFAULT_FILE_NAME, ci.getMimeType(), imageAvatar)) { log.warn("Profile2 image converter: Saving avatar profile image failed. Main image will be used instead."); } else { ci.setAvatarResourceId(avatarResourceId); ci.setNeedsSaving(true); } } /** * Private class to store some info while we perform the conversions * * @author Steve Swinsburg (steve.swinsburg@gmail.com) * */ class ConvertedImage { @Getter @Setter private String mainResourceId; @Getter @Setter private String thumbnailResourceId; @Getter @Setter private String avatarResourceId; @Getter @Setter private byte[] image; @Getter @Setter private String mimeType; @Getter @Setter private String userUuid; @Getter @Setter private String fileName; //only want to save if we have to, otherwise we will be duplicating records @Getter @Setter private boolean needsSaving = false; public boolean needsThumb() { return (validBytes() && StringUtils.isBlank(thumbnailResourceId)); } public boolean needsAvatar() { return (validBytes() && StringUtils.isBlank(avatarResourceId)); } public boolean validBytes() { return (image != null && image.length > 0); } } }