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}