001    /*
002     * Copyright 2011 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     */
016    package org.kuali.kfs.sys.batch.service.impl;
017    
018    import java.io.ByteArrayInputStream;
019    import java.io.File;
020    import java.io.FileNotFoundException;
021    import java.io.FileWriter;
022    import java.io.FilenameFilter;
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.net.MalformedURLException;
026    import java.net.URL;
027    import java.util.ArrayList;
028    import java.util.List;
029    
030    import javax.xml.XMLConstants;
031    import javax.xml.transform.Source;
032    import javax.xml.transform.stream.StreamSource;
033    import javax.xml.validation.Schema;
034    import javax.xml.validation.SchemaFactory;
035    import javax.xml.validation.Validator;
036    
037    import org.apache.commons.digester.Digester;
038    import org.apache.commons.digester.Rules;
039    import org.apache.commons.digester.xmlrules.DigesterLoader;
040    import org.apache.commons.lang.StringUtils;
041    import org.kuali.kfs.sys.KFSConstants;
042    import org.kuali.kfs.sys.KFSKeyConstants;
043    import org.kuali.kfs.sys.KFSConstants.SystemGroupParameterNames;
044    import org.kuali.kfs.sys.batch.BatchInputFileType;
045    import org.kuali.kfs.sys.batch.service.BatchInputFileService;
046    import org.kuali.kfs.sys.context.SpringContext;
047    import org.kuali.kfs.sys.exception.FileStorageException;
048    import org.kuali.kfs.sys.exception.ParseException;
049    import org.kuali.kfs.sys.exception.XmlErrorHandler;
050    import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
051    import org.kuali.rice.kim.bo.Person;
052    import org.kuali.rice.kim.service.KIMServiceLocator;
053    import org.kuali.rice.kns.exception.AuthorizationException;
054    import org.kuali.rice.kns.service.ParameterService;
055    import org.kuali.rice.kns.util.GlobalVariables;
056    import org.kuali.rice.kns.util.ObjectUtils;
057    import org.springframework.core.io.UrlResource;
058    import org.xml.sax.SAXException;
059    
060    /**
061     * Provides batch input file management, including listing files, parsing, downloading, storing, and deleting.
062     */
063    public class BatchInputFileServiceImpl implements BatchInputFileService {
064        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BatchInputFileServiceImpl.class);
065    
066        /**
067         * Delegates to the batch input file type to parse the file.
068         * 
069         * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#parse(org.kuali.kfs.sys.batch.BatchInputFileType, byte[])
070         */
071        public Object parse(BatchInputFileType batchInputFileType, byte[] fileByteContent) {
072            try {
073                return batchInputFileType.parse(fileByteContent);
074            }
075            catch (ParseException e) {
076                LOG.error("Error encountered parsing file", e);
077                throw e;
078            }
079        }
080    
081        /**
082         * Defers to batch type to do any validation on the parsed contents.
083         * 
084         * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#validate(org.kuali.kfs.sys.batch.BatchInputFileType, java.lang.Object)
085         */
086        public boolean validate(BatchInputFileType batchInputFileType, Object parsedObject) {
087            if (batchInputFileType == null || parsedObject == null) {
088                LOG.error("an invalid(null) argument was given");
089                throw new IllegalArgumentException("an invalid(null) argument was given");
090            }
091    
092            boolean contentsValid = true;
093            contentsValid = batchInputFileType.validate(parsedObject);
094            return contentsValid;
095        }
096    
097        /**
098         * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#save(org.kuali.rice.kim.bo.Person,
099         *      org.kuali.kfs.sys.batch.BatchInputFileType, java.lang.String, java.io.InputStream)
100         */
101        public String save(Person user, BatchInputFileType batchInputFileType, String fileUserIdentifier, InputStream fileContents, Object parsedObject) throws AuthorizationException, FileStorageException {
102            if (user == null || batchInputFileType == null || fileContents == null) {
103                LOG.error("an invalid(null) argument was given");
104                throw new IllegalArgumentException("an invalid(null) argument was given");
105            }
106    
107            if (!isFileUserIdentifierProperlyFormatted(fileUserIdentifier)) {
108                LOG.error("The following file user identifer was not properly formatted: " + fileUserIdentifier);
109                throw new IllegalArgumentException("The following file user identifer was not properly formatted: " + fileUserIdentifier);
110            }
111    
112            // defer to batch input type to add any security or other needed information to the file name
113            String saveFileName = batchInputFileType.getDirectoryPath() + "/" + batchInputFileType.getFileName(user.getPrincipalName(), parsedObject, fileUserIdentifier);
114            saveFileName += "." + batchInputFileType.getFileExtension();
115    
116            // consruct the file object and check for existence
117            File fileToSave = new File(saveFileName);
118            if (fileToSave.exists()) {
119                LOG.error("cannot store file, name already exists " + saveFileName);
120                throw new FileStorageException("Cannot store file because the name " + saveFileName + " already exists on the file system.");
121            }
122    
123            try {
124                FileWriter fileWriter = new FileWriter(fileToSave);
125                while (fileContents.available() > 0) {
126                    fileWriter.write(fileContents.read());
127                }
128                fileWriter.flush();
129                fileWriter.close();
130    
131                createDoneFile(fileToSave);
132                
133                batchInputFileType.process(saveFileName, parsedObject);
134            }
135            catch (IOException e) {
136                LOG.error("unable to save contents to file " + saveFileName, e);
137                throw new RuntimeException("errors encountered while writing file " + saveFileName, e);
138            }
139    
140            return saveFileName;
141        }
142    
143        /**
144         * Creates a '.done' file with the name of the batch file.
145         */
146        protected void createDoneFile(File batchFile) {
147            File doneFile = generateDoneFileObject(batchFile);
148            String doneFileName = doneFile.getName();
149    
150            if (!doneFile.exists()) {
151                boolean doneFileCreated = false;
152                try {
153                    doneFileCreated = doneFile.createNewFile();
154                }
155                catch (IOException e) {
156                    LOG.error("unable to create done file " + doneFileName, e);
157                    throw new RuntimeException("Errors encountered while saving the file: Unable to create .done file " + doneFileName, e);
158                }
159    
160                if (!doneFileCreated) {
161                    LOG.error("unable to create done file " + doneFileName);
162                    throw new RuntimeException("Errors encountered while saving the file: Unable to create .done file " + doneFileName);
163                }
164            }
165        }
166    
167        /**
168         * This method is responsible for creating a File object that represents the done file. The real file represented on disk may
169         * not exist
170         * 
171         * @param batchInputFile
172         * @return a File object representing the done file. The real file may not exist on disk, but the return value can be used to
173         *         create that file.
174         */
175        protected File generateDoneFileObject(File batchInputFile) {
176            String doneFileName = StringUtils.substringBeforeLast(batchInputFile.getPath(), ".") + ".done";
177            File doneFile = new File(doneFileName);
178            return doneFile;
179        }
180    
181        /**
182         * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#isBatchInputTypeActive(org.kuali.kfs.sys.batch.BatchInputFileType)
183         */
184        public boolean isBatchInputTypeActive(BatchInputFileType batchInputFileType) {
185            if (batchInputFileType == null) {
186                LOG.error("an invalid(null) argument was given");
187                throw new IllegalArgumentException("an invalid(null) argument was given");
188            }
189    
190            List<String> activeInputTypes = SpringContext.getBean(ParameterService.class).getParameterValues(KfsParameterConstants.FINANCIAL_SYSTEM_BATCH.class, SystemGroupParameterNames.ACTIVE_INPUT_TYPES_PARAMETER_NAME);
191    
192            boolean activeBatchType = false;
193            if (activeInputTypes.size() > 0 && activeInputTypes.contains(batchInputFileType.getFileTypeIdentifer())) {
194                activeBatchType = true;
195            }
196    
197            return activeBatchType;
198        }
199    
200        /**
201         * Fetches workgroup for batch type from system parameter and verifies user is a member. Then a list of all files for the batch
202         * type are retrieved. For each file, the file and user is sent through the checkAuthorization method of the batch input type
203         * implementation for finer grained security. If the method returns true, the filename is added to the user's list.
204         * 
205         * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#listBatchTypeFilesForUser(org.kuali.kfs.sys.batch.BatchInputFileType,
206         *      org.kuali.rice.kim.bo.Person)
207         */
208        public List<String> listBatchTypeFilesForUser(BatchInputFileType batchInputFileType, Person user) throws AuthorizationException {
209            if (batchInputFileType == null || user == null) {
210                LOG.error("an invalid(null) argument was given");
211                throw new IllegalArgumentException("an invalid(null) argument was given");
212            }
213    
214            File[] filesInBatchDirectory = listFilesInBatchTypeDirectory(batchInputFileType);
215    
216            List<String> userFileNamesList = new ArrayList<String>();
217            List<File> userFileList = listBatchTypeFilesForUserAsFiles(batchInputFileType, user);
218    
219            for (File userFile : userFileList) {
220                userFileNamesList.add(userFile.getAbsolutePath());
221            }
222    
223            return userFileNamesList;
224        }
225    
226        protected List<File> listBatchTypeFilesForUserAsFiles(BatchInputFileType batchInputFileType, Person user) throws AuthorizationException {
227            File[] filesInBatchDirectory = listFilesInBatchTypeDirectory(batchInputFileType);
228    
229            List<File> userFileList = new ArrayList<File>();
230            if (filesInBatchDirectory != null) {
231                for (int i = 0; i < filesInBatchDirectory.length; i++) {
232                    File batchFile = filesInBatchDirectory[i];
233                    String fileExtension = StringUtils.substringAfterLast(batchFile.getName(), ".");
234                    if (batchInputFileType.getFileExtension().equals(fileExtension)) {
235                        if (user.getPrincipalName().equals(batchInputFileType.getAuthorPrincipalName(batchFile))) {
236                            userFileList.add(batchFile);
237                        }
238                    }
239                }
240            }
241            return userFileList;
242        }
243    
244        /**
245         * Returns List of filenames for existing files in the directory given by the batch input type.
246         */
247        protected File[] listFilesInBatchTypeDirectory(BatchInputFileType batchInputFileType) {
248            File batchTypeDirectory = new File(batchInputFileType.getDirectoryPath());
249            return batchTypeDirectory.listFiles();
250        }
251    
252        /**
253         * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#listInputFileNamesWithDoneFile(org.kuali.kfs.sys.batch.BatchInputFileType)
254         */
255        public List<String> listInputFileNamesWithDoneFile(BatchInputFileType batchInputFileType) {
256            if (batchInputFileType == null) {
257                LOG.error("an invalid(null) argument was given");
258                throw new IllegalArgumentException("an invalid(null) argument was given");
259            }
260    
261            File batchTypeDirectory = new File(batchInputFileType.getDirectoryPath());
262            File[] doneFiles = batchTypeDirectory.listFiles(new DoneFilenameFilter());
263    
264            List<String> batchInputFiles = new ArrayList<String>();
265            for (int i = 0; i < doneFiles.length; i++) {
266                File doneFile = doneFiles[i];
267                File dataFile = new File(StringUtils.substringBeforeLast(doneFile.getPath(), ".") + "." + batchInputFileType.getFileExtension());
268                if (dataFile.exists()) {
269                    batchInputFiles.add(dataFile.getPath());
270                }
271            }
272    
273            return batchInputFiles;
274        }
275    
276        /**
277         * Retrieves files in a directory with the .done extension.
278         */
279        protected class DoneFilenameFilter implements FilenameFilter {
280            /**
281             * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String)
282             */
283            public boolean accept(File dir, String name) {
284                return name.endsWith(".done");
285            }
286        }
287    
288        /**
289         * For this implementation, a file user identifier must consist of letters and digits
290         * 
291         * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#isFileUserIdentifierProperlyFormatted(java.lang.String)
292         */
293        public boolean isFileUserIdentifierProperlyFormatted(String fileUserIdentifier) {
294            if(ObjectUtils.isNull(fileUserIdentifier)) {
295                return false;
296            }
297            for (int i = 0; i < fileUserIdentifier.length(); i++) {
298                char c = fileUserIdentifier.charAt(i);
299                if (!(Character.isLetterOrDigit(c))) {
300                    return false;
301                }
302            }
303            return true;
304        }
305    }
306