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