package ru.hflabs.rcd.web.controller.document; import com.google.common.base.Charsets; import com.google.common.collect.*; import com.google.common.net.HttpHeaders; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.FileCopyUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import ru.hflabs.rcd.Directories; import ru.hflabs.rcd.backend.console.exports.ExportDictionaries; import ru.hflabs.rcd.backend.console.exports.handlers.ExportDescriptor; import ru.hflabs.rcd.backend.console.exports.handlers.dictionary.ExportDictionariesCommand; import ru.hflabs.rcd.backend.console.imports.ImportDictionaries; import ru.hflabs.rcd.backend.console.imports.handlers.ImportDescriptor; import ru.hflabs.rcd.backend.console.imports.handlers.dictionary.ImportDictionariesCommand; import ru.hflabs.rcd.backend.console.preference.FilePreference; import ru.hflabs.rcd.connector.files.FilesByExtensionFilter; import ru.hflabs.rcd.exception.ApplicationException; import ru.hflabs.rcd.exception.constraint.IllegalPrimaryKeyException; import ru.hflabs.rcd.exception.transfer.CommunicationException; import ru.hflabs.rcd.model.criteria.FilterCriteria; import ru.hflabs.rcd.model.criteria.FilterCriteriaValue; import ru.hflabs.rcd.model.criteria.FilterResult; import ru.hflabs.rcd.model.definition.ModelDefinition; import ru.hflabs.rcd.model.document.Dictionary; import ru.hflabs.rcd.model.document.Group; import ru.hflabs.rcd.model.document.MetaField; import ru.hflabs.rcd.model.path.DictionaryNamedPath; import ru.hflabs.rcd.service.IPagingService; import ru.hflabs.rcd.service.IServiceFactory; import ru.hflabs.rcd.service.document.IDictionaryService; import ru.hflabs.rcd.service.document.IGroupService; import ru.hflabs.rcd.service.document.IMetaFieldService; import ru.hflabs.rcd.web.controller.ControllerTemplate; import ru.hflabs.rcd.web.model.PageRequestBean; import ru.hflabs.rcd.web.model.PageResponseBean; import ru.hflabs.rcd.web.model.document.DictionaryBean; import ru.hflabs.rcd.web.model.transfer.DownloadDictionaryDescriptor; import ru.hflabs.rcd.web.model.transfer.UploadDictionaryDescriptor; import ru.hflabs.util.core.FormatUtil; import ru.hflabs.util.io.IOUtils; import ru.hflabs.util.spring.Assert; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; import static com.google.common.net.MediaType.CSV_UTF_8; import static com.google.common.net.MediaType.HTML_UTF_8; import static java.net.URLDecoder.decode; import static java.net.URLEncoder.encode; import static ru.hflabs.rcd.accessor.Accessors.*; import static ru.hflabs.rcd.service.ServiceUtils.extractSingleDocument; import static ru.hflabs.rcd.web.PagingUtils.findPageByCriteria; /** * Класс <class>DictionaryController</class> реализует контроллер управления справочниками * * @see Dictionary */ @Controller(DictionaryController.MAPPING_URI + DictionaryController.NAME_POSTFIX) @RequestMapping(DictionaryController.MAPPING_URI + DictionaryController.DATA_URI) public class DictionaryController extends ControllerTemplate { public static final String MAPPING_URI = "dictionaries"; /** Сервис работы с группами справочников */ @Resource(name = "groupService") private IGroupService groupService; /** Сервис работы со справочниками */ @Resource(name = "dictionaryService") private IDictionaryService dictionaryService; /** Сервис работы с МЕТА-полями справочника */ @Resource(name = "metaFieldService") private IMetaFieldService metaFieldService; /** Сервис импорта справочников */ @Resource(name = "importDictionaries") private ImportDictionaries importDictionaries; /** Сервис экспорта справочников */ @Resource(name = "exportDictionaries") private ExportDictionaries exportDictionaries; /** * Возвращает модели загрузки и сохранения справочника * * @param modelDefinitionFactory фабрика построения моделей * @return Возвращает коллекцию моделей */ public static Collection<ModelDefinition> createTransferModel(IServiceFactory<ModelDefinition, Class<?>> modelDefinitionFactory) { // Параметры доступа к файлу по умолчанию Map<String, Object> fileDefaultParameters = ImmutableMap.<String, Object>builder() .put(FilePreference.ENCODING, FilePreference.DEFAULT_ENCODING) .put(FilePreference.DELIMITER, FilePreference.DEFAULT_DELIMITER) .put(FilePreference.QUOTE, FilePreference.DEFAULT_QUOTE) .build(); Map<String, Object> fileAvailableValues = ImmutableMap.<String, Object>builder() .put(FilePreference.ENCODING, IOUtils.availableEncodingNames(true)) .build(); // Дескриптор загрузки ModelDefinition uploadDictionaryDefinition = modelDefinitionFactory.retrieveService(UploadDictionaryDescriptor.class); { uploadDictionaryDefinition.setDefaultParameters(fileDefaultParameters); uploadDictionaryDefinition.setAvailableValues(fileAvailableValues); } // Дескриптор сохранения ModelDefinition downloadDictionaryDefinition = modelDefinitionFactory.retrieveService(DownloadDictionaryDescriptor.class); { downloadDictionaryDefinition.setDefaultParameters(fileDefaultParameters); downloadDictionaryDefinition.setAvailableValues(fileAvailableValues); } // Возвращаем модели загрузки и сохранения справочника return Arrays.asList(uploadDictionaryDefinition, downloadDictionaryDefinition); } @RequestMapping(value = "/model", method = RequestMethod.GET) @ResponseBody public Collection<ModelDefinition> createModel() { return ImmutableList.<ModelDefinition>builder() .add(modelDefinitionFactory.retrieveService(Dictionary.class)) .addAll(createTransferModel(modelDefinitionFactory)) .build(); } @RequestMapping(value = "/", method = RequestMethod.GET) @ResponseBody public PageResponseBean<DictionaryBean> getDictionaries( @RequestParam(value = Dictionary.GROUP_ID, required = false) final String groupId, @Valid @ModelAttribute final PageRequestBean page) { // Форматируем параметры final String targetGroupId = FormatUtil.parseString(groupId); // Выполняем поиск страницы справочников return findPageByCriteria(page.getPageSize(), defaultPagingSize, page.getPage(), new IPagingService<DictionaryBean>() { @Override public FilterResult<DictionaryBean> findPage(int count, int offset) { // Формируем критерий FilterCriteria filterCriteria = page.createFilterCriteria() .injectOffset(offset) .injectCount(count); // Устанавливаем группу if (targetGroupId != null) { filterCriteria.injectFilters( ImmutableMap.<String, FilterCriteriaValue<?>>of( Dictionary.GROUP_ID, new FilterCriteriaValue.StringValue(targetGroupId) ) ); } // Выполняем поиск FilterResult<Dictionary> dictionaries = dictionaryService.findByCriteria(filterCriteria, true); // Формируем декораторы Collection<DictionaryBean> result = Lists.newArrayList( Collections2.transform(dictionaries.getResult(), DictionaryBean.CONVERT) ); // Возвращаем результат фильтрации return new FilterResult<>(result, dictionaries.getCountByFilter(), dictionaries.getTotalCount()); } }); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) @ResponseBody public DictionaryBean getDictionary(@PathVariable String id) { return DictionaryBean.CONVERT.apply( dictionaryService.findByID(id, true, false) ); } @RequestMapping(value = "/", method = RequestMethod.POST) @ResponseBody @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Throwable.class) public DictionaryBean createDictionary(@RequestBody Dictionary dictionary) { // Выполняем создание справочника Dictionary result = createSingleDocument(dictionaryService, injectId(dictionary, null), true); // Выполняем создание первичного МЕТА-поля справочника MetaField metaField = new MetaField(); { metaField = linkRelative(dictionary, metaField); metaField = injectName(metaField, MetaField.DEFAULT_NAME); metaField.setOrdinal(MetaField.DEFAULT_ORDINAL); metaField.establishFlags(MetaField.FLAG_PRIMARY); } createSingleDocument(metaFieldService, metaField, false); // Возвращаем созданный справочник return DictionaryBean.CONVERT.apply(result); } @RequestMapping(value = "/{id}", method = RequestMethod.PUT) @ResponseBody public DictionaryBean updateDictionary(@PathVariable String id, @RequestBody Dictionary dictionary) { return DictionaryBean.CONVERT.apply( updateSingleDocument(dictionaryService, injectId(dictionary, id), true) ); } @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public void closeDictionary(@PathVariable String id) { dictionaryService.closeByIDs(Sets.newHashSet(id)); } @RequestMapping(value = "/download/{dictionaryId}", method = RequestMethod.GET) public void downloadDictionary(@PathVariable String dictionaryId, @Valid @ModelAttribute DownloadDictionaryDescriptor bean, HttpServletResponse response, Locale locale) throws Throwable { final File directory = new File(Directories.TMP_FOLDER.getLocation(), String.valueOf(System.nanoTime())); // Получаем целевой справочник final Dictionary targetDictionary = dictionaryService.findByID(dictionaryId, true, false); try { // Формируем параметры экспорта ExportDictionariesCommand command = bean.getDelegate(); command.setGroupName(targetDictionary.getRelative().getName()); command.setDictionaryName(targetDictionary.getName()); command.setFileType(FilesByExtensionFilter.CSV); // Устанавливаем директорию экспорта command.setTargetPath(directory.getCanonicalPath()); // Выполняем экспорт ExportDescriptor descriptor = exportDictionaries.exportDocuments(command); if (!descriptor.hasErrors()) { // Получаем файл справочника DictionaryNamedPath path = new DictionaryNamedPath(command.getGroupName(), command.getDictionaryName()); File exportedFile = descriptor.getFiles().get(path); Assert.notNull(exportedFile, String.format("Can't find file for dictionary %s", path)); // Форминуем название файла String fileName = decode(encode(exportedFile.getName(), Charsets.UTF_8.name()), Charsets.ISO_8859_1.name()); // Формируем контент ответа response.setContentType(CSV_UTF_8.toString()); response.setContentLength((int) exportedFile.length()); // Форминуем заголовок ответа response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", fileName)); // Выполняем копирование результирующего файла в поток вывода FileCopyUtils.copy(new FileInputStream(exportedFile), response.getOutputStream()); } } catch (Throwable ex) { throw new CommunicationException(messageSource.getMessage("CommunicationException.downloadDictionaryDescriptor", null, locale)); } finally { FileUtils.deleteQuietly(directory); } } /** * Выполняет трансфер импортируемого файла * * @param file файл импорта * @return Возвращает путь к временному файлу */ private String doTransferFile(MultipartFile file) throws IOException { String prefix = FilenameUtils.getBaseName(file.getOriginalFilename()) + "_" + System.nanoTime() + "_"; String suffix = "." + FilenameUtils.getExtension(file.getOriginalFilename()); File targetFile = File.createTempFile(prefix, suffix, Directories.TMP_FOLDER.getLocation()); file.transferTo(targetFile); return targetFile.getCanonicalPath(); } /** * Выполняет загрузку справочника * * @param command подготовленная команда * @param path именованный путь справочника * @param file целевой файл */ private DictionaryBean doUploadDictionary(ImportDictionariesCommand command, DictionaryNamedPath path, String description, MultipartFile file, Locale locale) throws Exception { try { command.setGroupName(path.getGroupName()); command.setDictionaryName(path.getDictionaryName()); command.setDictionaryDescription(description); // Выполняем передачу целевого файла command.setTargetPath(doTransferFile(file)); // Выполняем импорт справочника ImportDescriptor<Group> descriptor = importDictionaries.importDocuments(command); // Проверяем ошибки импорта if (descriptor.hasErrors()) { throw new CommunicationException(messageSource.getMessage("CommunicationException.uploadDictionaryDescriptor", null, locale)); } // Получаем загруженный справочник Group group = extractSingleDocument(descriptor.getDocuments()); Dictionary dictionary = extractSingleDocument(group.getDescendants()); // Формируем и возвращаем декоратор return DictionaryBean.CONVERT.apply(dictionary); } finally { FileUtils.deleteQuietly(command.retrieveTargetFile()); } } /** * <b>Workaround для IE9+</b> * <p/> * Статус ответа должен быть всегда {@link HttpServletResponse#SC_OK OK} и Content-Type: text/html, * для того, чтобы браузер не предлагал сохранить возвращаемый контент */ private void doUploadDictionary(BindingResult bindingResult, Callable<DictionaryBean> callback, HttpServletResponse response, Locale locale) throws Exception { Object result; if (bindingResult.hasErrors()) { result = doHandleValidationException(bindingResult, locale); } else { try { result = callback.call(); } catch (IllegalPrimaryKeyException ex) { result = handleIllegalPrimaryKeyException(ex, locale); } catch (ApplicationException ex) { result = handleApplicationException(ex, locale); } catch (Throwable th) { result = handleThrowable(th, locale); } } response.setHeader(HttpHeaders.CONTENT_TYPE, HTML_UTF_8.toString()); objectMapper.writeValue(response.getOutputStream(), result); } @RequestMapping(value = "/upload", method = RequestMethod.POST) public void uploadDictionary( @Valid @ModelAttribute final UploadDictionaryDescriptor bean, BindingResult bindingResult, @RequestPart final MultipartFile file, HttpServletResponse response, final Locale locale) throws Throwable { Callable<DictionaryBean> callback = new Callable<DictionaryBean>() { @Override public DictionaryBean call() throws Exception { Group targetGroup = groupService.findByID(bean.getGroupId(), true, false); DictionaryNamedPath path = new DictionaryNamedPath(targetGroup.getName(), bean.getName()); return doUploadDictionary(bean.getDelegate(), path, bean.getDescription(), file, locale); } }; doUploadDictionary(bindingResult, callback, response, locale); } @RequestMapping(value = "/upload/{dictionaryId}", method = RequestMethod.POST) public void uploadDictionary( @PathVariable final String dictionaryId, @Valid @ModelAttribute final UploadDictionaryDescriptor bean, BindingResult bindingResult, @RequestPart final MultipartFile file, HttpServletResponse response, final Locale locale) throws Throwable { Callable<DictionaryBean> callback = new Callable<DictionaryBean>() { @Override public DictionaryBean call() throws Exception { Dictionary targetDictionary = dictionaryService.findByID(dictionaryId, true, false); DictionaryNamedPath path = new DictionaryNamedPath(targetDictionary.getRelative().getName(), targetDictionary.getName()); return doUploadDictionary(bean.getDelegate(), path, targetDictionary.getDescription(), file, locale); } }; doUploadDictionary(bindingResult, callback, response, locale); } }