View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.service.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.core.api.config.property.ConfigurationService;
21  import org.kuali.rice.krad.bo.Attachment;
22  import org.kuali.rice.krad.bo.Note;
23  import org.kuali.rice.krad.bo.PersistableBusinessObject;
24  import org.kuali.rice.krad.dao.AttachmentDao;
25  import org.kuali.rice.krad.document.Document;
26  import org.kuali.rice.krad.service.AttachmentService;
27  import org.kuali.rice.krad.util.KRADConstants;
28  import org.springframework.transaction.annotation.Transactional;
29  
30  import java.io.BufferedInputStream;
31  import java.io.BufferedOutputStream;
32  import java.io.File;
33  import java.io.FileInputStream;
34  import java.io.FileOutputStream;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.util.UUID;
38  
39  /**
40   * Attachment service implementation
41   */
42  @Transactional
43  public class AttachmentServiceImpl implements AttachmentService {
44  	private static final int MAX_DIR_LEVELS = 6;
45      private static Logger LOG = Logger.getLogger(AttachmentServiceImpl.class);
46  
47      private ConfigurationService kualiConfigurationService;
48      private AttachmentDao attachmentDao;
49      /**
50       * Retrieves an Attachment by note identifier.
51       * 
52       * @see org.kuali.rice.krad.service.AttachmentService#getAttachmentByNoteId(java.lang.Long)
53       */
54      public Attachment getAttachmentByNoteId(Long noteId) {
55  		return attachmentDao.getAttachmentByNoteId(noteId);
56  	}
57  
58      /**
59       * @see org.kuali.rice.krad.service.AttachmentService#createAttachment(org.kuali.rice.krad.bo.PersistableBusinessObject,
60       * String, String, int, java.io.InputStream, String)
61       */
62      public Attachment createAttachment(PersistableBusinessObject parent, String uploadedFileName, String mimeType, int fileSize, InputStream fileContents, String attachmentTypeCode) throws IOException {
63          if ( LOG.isDebugEnabled() ) {
64              LOG.debug("starting to create attachment for document: " + parent.getObjectId());
65          }
66          if (parent == null) {
67              throw new IllegalArgumentException("invalid (null or uninitialized) document");
68          }
69          if (StringUtils.isBlank(uploadedFileName)) {
70              throw new IllegalArgumentException("invalid (blank) fileName");
71          }
72          if (StringUtils.isBlank(mimeType)) {
73              throw new IllegalArgumentException("invalid (blank) mimeType");
74          }
75          if (fileSize <= 0) {
76              throw new IllegalArgumentException("invalid (non-positive) fileSize");
77          }
78          if (fileContents == null) {
79              throw new IllegalArgumentException("invalid (null) inputStream");
80          }
81  
82          String uniqueFileNameGuid = UUID.randomUUID().toString();
83          String fullPathUniqueFileName = getDocumentDirectory(parent.getObjectId()) + File.separator + uniqueFileNameGuid;
84  
85          writeInputStreamToFileStorage(fileContents, fullPathUniqueFileName);
86  
87          // create DocumentAttachment
88          Attachment attachment = new Attachment();
89          attachment.setAttachmentIdentifier(uniqueFileNameGuid);
90          attachment.setAttachmentFileName(uploadedFileName);
91          attachment.setAttachmentFileSize(new Long(fileSize));
92          attachment.setAttachmentMimeTypeCode(mimeType);
93          attachment.setAttachmentTypeCode(attachmentTypeCode);
94          
95          LOG.debug("finished creating attachment for document: " + parent.getObjectId());
96          return attachment;
97      }
98  
99      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.AttachmentService#retrieveAttachmentContents(org.kuali.rice.krad.bo.Attachment)
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 }