001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.service.impl; 017 018import java.io.BufferedInputStream; 019import java.io.BufferedOutputStream; 020import java.io.File; 021import java.io.FileInputStream; 022import java.io.FileOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.util.UUID; 026 027import org.apache.commons.lang.StringUtils; 028import org.apache.log4j.Logger; 029import org.kuali.rice.core.api.config.property.ConfigurationService; 030import org.kuali.rice.core.api.mo.common.GloballyUnique; 031import org.kuali.rice.krad.bo.Attachment; 032import org.kuali.rice.krad.bo.Note; 033import org.kuali.rice.krad.data.DataObjectService; 034import org.kuali.rice.krad.service.AttachmentService; 035import org.kuali.rice.krad.util.KRADConstants; 036import org.springframework.beans.factory.annotation.Required; 037import org.springframework.transaction.annotation.Transactional; 038 039/** 040 * Attachment service implementation 041 */ 042@Transactional 043public class AttachmentServiceImpl implements AttachmentService { 044 private static final int MAX_DIR_LEVELS = 6; 045 private static final Logger LOG = Logger.getLogger(AttachmentServiceImpl.class); 046 047 protected ConfigurationService kualiConfigurationService; 048 protected DataObjectService dataObjectService; 049 050 /** 051 * Retrieves an Attachment by note identifier. 052 * 053 * @see org.kuali.rice.krad.service.AttachmentService#getAttachmentByNoteId(java.lang.Long) 054 */ 055 @Override 056 public Attachment getAttachmentByNoteId(Long noteId) { 057 if(noteId == null){ 058 return null; 059 } 060 return dataObjectService.find(Attachment.class, noteId); 061 } 062 063 /** 064 * @see org.kuali.rice.krad.service.AttachmentService#createAttachment(GloballyUnique, 065 * String, String, int, java.io.InputStream, String) 066 */ 067 @Override 068 public Attachment createAttachment(GloballyUnique parent, String uploadedFileName, String mimeType, int fileSize, InputStream fileContents, String attachmentTypeCode) throws IOException { 069 if ( LOG.isDebugEnabled() ) { 070 LOG.debug("starting to create attachment for document: " + parent.getObjectId()); 071 } 072 if (parent == null) { 073 throw new IllegalArgumentException("invalid (null or uninitialized) document"); 074 } 075 if (StringUtils.isBlank(uploadedFileName)) { 076 throw new IllegalArgumentException("invalid (blank) fileName"); 077 } 078 if (StringUtils.isBlank(mimeType)) { 079 throw new IllegalArgumentException("invalid (blank) mimeType"); 080 } 081 if (fileSize <= 0) { 082 throw new IllegalArgumentException("invalid (non-positive) fileSize"); 083 } 084 if (fileContents == null) { 085 throw new IllegalArgumentException("invalid (null) inputStream"); 086 } 087 088 String uniqueFileNameGuid = UUID.randomUUID().toString(); 089 String fullPathUniqueFileName = getDocumentDirectory(parent.getObjectId()) + File.separator + uniqueFileNameGuid; 090 091 writeInputStreamToFileStorage(fileContents, fullPathUniqueFileName); 092 093 // create DocumentAttachment 094 Attachment attachment = new Attachment(); 095 attachment.setAttachmentIdentifier(uniqueFileNameGuid); 096 attachment.setAttachmentFileName(uploadedFileName); 097 attachment.setAttachmentFileSize(new Long(fileSize)); 098 attachment.setAttachmentMimeTypeCode(mimeType); 099 attachment.setAttachmentTypeCode(attachmentTypeCode); 100 101 if ( LOG.isDebugEnabled() ) { 102 LOG.debug("finished creating attachment for document: " + parent.getObjectId()); 103 } 104 return attachment; 105 } 106 107 private void writeInputStreamToFileStorage(InputStream fileContents, String fullPathUniqueFileName) throws IOException { 108 File fileOut = new File(fullPathUniqueFileName); 109 FileOutputStream streamOut = null; 110 BufferedOutputStream bufferedStreamOut = null; 111 try { 112 streamOut = new FileOutputStream(fileOut); 113 bufferedStreamOut = new BufferedOutputStream(streamOut); 114 int c; 115 while ((c = fileContents.read()) != -1) { 116 bufferedStreamOut.write(c); 117 } 118 } 119 finally { 120 bufferedStreamOut.close(); 121 streamOut.close(); 122 } 123 } 124 125 @Override 126 public void moveAttachmentWherePending(Note note) { 127 if (note == null) { 128 throw new IllegalArgumentException("Note must be non-null"); 129 } 130 if (StringUtils.isBlank(note.getObjectId())) { 131 throw new IllegalArgumentException("Note does not have a valid object id, object id was null or empty"); 132 } 133 Attachment attachment = note.getAttachment(); 134 if(attachment!=null){ 135 try { 136 moveAttachmentFromPending(attachment, note.getRemoteObjectIdentifier()); 137 } 138 catch (IOException e) { 139 throw new RuntimeException("Problem moving pending attachment to final directory"); 140 } 141 } 142 } 143 144 private void moveAttachmentFromPending(Attachment attachment, String objectId) throws IOException { 145 //This method would probably be more efficient if attachments had a pending flag 146 String fullPendingFileName = getPendingDirectory() + File.separator + attachment.getAttachmentIdentifier(); 147 File pendingFile = new File(fullPendingFileName); 148 149 if(pendingFile.exists()) { 150 BufferedInputStream bufferedStream = null; 151 FileInputStream oldFileStream = null; 152 String fullPathNewFile = getDocumentDirectory(objectId) + File.separator + attachment.getAttachmentIdentifier(); 153 try { 154 oldFileStream = new FileInputStream(pendingFile); 155 bufferedStream = new BufferedInputStream(oldFileStream); 156 writeInputStreamToFileStorage(bufferedStream,fullPathNewFile); 157 } 158 finally { 159 160 bufferedStream.close(); 161 oldFileStream.close(); 162 //this has to come after the close 163 pendingFile.delete(); 164 165 } 166 } 167 168 } 169 170 @Override 171 public void deleteAttachmentContents(Attachment attachment) { 172 if (attachment.getNote() == null) throw new RuntimeException("Attachment.note must be set in order to delete the attachment"); 173 String fullPathUniqueFileName = getDocumentDirectory(attachment.getNote().getRemoteObjectIdentifier()) + File.separator + attachment.getAttachmentIdentifier(); 174 File attachmentFile = new File(fullPathUniqueFileName); 175 attachmentFile.delete(); 176 } 177 private String getPendingDirectory() { 178 return this.getDocumentDirectory(""); 179 } 180 181 private String getDocumentDirectory(String objectId) { 182 // Create a directory; all ancestor directories must exist 183 File documentDirectory = new File(getDocumentFileStorageLocation(objectId)); 184 if (!documentDirectory.exists()) { 185 boolean success = documentDirectory.mkdirs(); 186 if (!success) { 187 throw new RuntimeException("Could not generate directory for File at: " + documentDirectory.getAbsolutePath()); 188 } 189 } 190 return documentDirectory.getAbsolutePath(); 191 } 192 193 /** 194 * /* (non-Javadoc) 195 * 196 * @see org.kuali.rice.krad.service.AttachmentService#retrieveAttachmentContents(org.kuali.rice.krad.bo.Attachment) 197 */ 198 @Override 199 public InputStream retrieveAttachmentContents(Attachment attachment) throws IOException { 200 String parentDirectory = ""; 201 if(attachment.getNote()!=null && attachment.getNote().getRemoteObjectIdentifier() != null) { 202 parentDirectory = attachment.getNote().getRemoteObjectIdentifier(); 203 } 204 205 return new BufferedInputStream(new FileInputStream(getDocumentDirectory(parentDirectory) + File.separator + attachment.getAttachmentIdentifier())); 206 } 207 208 private String getDocumentFileStorageLocation(String objectId) { 209 String location = null; 210 if(StringUtils.isEmpty(objectId)) { 211 location = kualiConfigurationService.getPropertyValueAsString( 212 KRADConstants.ATTACHMENTS_PENDING_DIRECTORY_KEY); 213 } else { 214 /* 215 * We need to create a hierarchical directory structure to store 216 * attachment directories, as most file systems max out at 16k 217 * or 32k entries. If we use 6 levels of hierarchy, it allows 218 * hundreds of billions of attachment directories. 219 */ 220 char[] chars = objectId.toUpperCase().replace(" ", "").toCharArray(); 221 int count = chars.length < MAX_DIR_LEVELS ? chars.length : MAX_DIR_LEVELS; 222 223 StringBuffer prefix = new StringBuffer(); 224 for ( int i = 0; i < count; i++ ) 225 prefix.append(File.separator + chars[i]); 226 227 location = kualiConfigurationService.getPropertyValueAsString(KRADConstants.ATTACHMENTS_DIRECTORY_KEY) + prefix + File.separator + objectId; 228 } 229 return location; 230 } 231 232 /** 233 * @see org.kuali.rice.krad.service.AttachmentService#deletePendingAttachmentsModifiedBefore(long) 234 */ 235 @Override 236 public void deletePendingAttachmentsModifiedBefore(long modificationTime) { 237 String pendingAttachmentDirName = getPendingDirectory(); 238 if (StringUtils.isBlank(pendingAttachmentDirName)) { 239 throw new RuntimeException("Blank pending attachment directory name"); 240 } 241 File pendingAttachmentDir = new File(pendingAttachmentDirName); 242 if (!pendingAttachmentDir.exists()) { 243 throw new RuntimeException("Pending attachment directory does not exist"); 244 } 245 if (!pendingAttachmentDir.isDirectory()) { 246 throw new RuntimeException("Pending attachment directory is not a directory! " + pendingAttachmentDir.getAbsolutePath()); 247 } 248 249 File[] files = pendingAttachmentDir.listFiles(); 250 for (File file : files) { 251 if (!file.getName().equals("placeholder.txt")) { 252 if (file.lastModified() < modificationTime) { 253 file.delete(); 254 } 255 } 256 } 257 258 } 259 260 /** 261 * Gets the configService attribute. 262 * @return Returns the configService. 263 */ 264 public ConfigurationService getKualiConfigurationService() { 265 return kualiConfigurationService; 266 } 267 268 /** 269 * Sets the configService attribute value. 270 * @param configService The configService to set. 271 */ 272 @Required 273 public void setKualiConfigurationService(ConfigurationService configService) { 274 this.kualiConfigurationService = configService; 275 } 276 277 @Required 278 public void setDataObjectService(DataObjectService dataObjectService) { 279 this.dataObjectService = dataObjectService; 280 } 281 282}