View Javadoc
1   /**
2    * Copyright 2005-2014 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 java.io.BufferedInputStream;
19  import java.io.BufferedOutputStream;
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.util.UUID;
26  
27  import org.apache.commons.lang.StringUtils;
28  import org.apache.log4j.Logger;
29  import org.kuali.rice.core.api.config.property.ConfigurationService;
30  import org.kuali.rice.core.api.mo.common.GloballyUnique;
31  import org.kuali.rice.krad.bo.Attachment;
32  import org.kuali.rice.krad.bo.Note;
33  import org.kuali.rice.krad.data.DataObjectService;
34  import org.kuali.rice.krad.service.AttachmentService;
35  import org.kuali.rice.krad.util.KRADConstants;
36  import org.springframework.beans.factory.annotation.Required;
37  import org.springframework.transaction.annotation.Transactional;
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 final Logger LOG = Logger.getLogger(AttachmentServiceImpl.class);
46  
47      protected ConfigurationService kualiConfigurationService;
48      protected DataObjectService dataObjectService;
49  
50      /**
51       * Retrieves an Attachment by note identifier.
52       *
53       * @see org.kuali.rice.krad.service.AttachmentService#getAttachmentByNoteId(java.lang.Long)
54       */
55      @Override
56  	public Attachment getAttachmentByNoteId(Long noteId) {
57          if(noteId == null){
58              return null;
59          }
60          return dataObjectService.find(Attachment.class, noteId);
61  	}
62  
63      /**
64       * @see org.kuali.rice.krad.service.AttachmentService#createAttachment(GloballyUnique,
65       * String, String, int, java.io.InputStream, String)
66       */
67      @Override
68  	public Attachment createAttachment(GloballyUnique parent, String uploadedFileName, String mimeType, int fileSize, InputStream fileContents, String attachmentTypeCode) throws IOException {
69          if ( LOG.isDebugEnabled() ) {
70              LOG.debug("starting to create attachment for document: " + parent.getObjectId());
71          }
72          if (parent == null) {
73              throw new IllegalArgumentException("invalid (null or uninitialized) document");
74          }
75          if (StringUtils.isBlank(uploadedFileName)) {
76              throw new IllegalArgumentException("invalid (blank) fileName");
77          }
78          if (StringUtils.isBlank(mimeType)) {
79              throw new IllegalArgumentException("invalid (blank) mimeType");
80          }
81          if (fileSize <= 0) {
82              throw new IllegalArgumentException("invalid (non-positive) fileSize");
83          }
84          if (fileContents == null) {
85              throw new IllegalArgumentException("invalid (null) inputStream");
86          }
87  
88          String uniqueFileNameGuid = UUID.randomUUID().toString();
89          String fullPathUniqueFileName = getDocumentDirectory(parent.getObjectId()) + File.separator + uniqueFileNameGuid;
90  
91          writeInputStreamToFileStorage(fileContents, fullPathUniqueFileName);
92  
93          // create DocumentAttachment
94          Attachment attachment = new Attachment();
95          attachment.setAttachmentIdentifier(uniqueFileNameGuid);
96          attachment.setAttachmentFileName(uploadedFileName);
97          attachment.setAttachmentFileSize(new Long(fileSize));
98          attachment.setAttachmentMimeTypeCode(mimeType);
99          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 }