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}