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