diff --git a/README.md b/README.md index 6af42ed8588652fe834f8274892a257c074ed1f0..7f2bff69f57429e505a1ad9a1458cdbd633ff337 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,46 @@ [![star](https://gitee.com/wb04307201/file-preview-spring-boot-starter/badge/star.svg?theme=dark)](https://gitee.com/wb04307201/file-preview-spring-boot-starter) [![fork](https://gitee.com/wb04307201/file-preview-spring-boot-starter/badge/fork.svg?theme=dark)](https://gitee.com/wb04307201/file-preview-spring-boot-starter) [![star](https://img.shields.io/github/stars/wb04307201/file-preview-spring-boot-starter)](https://github.com/wb04307201/file-preview-spring-boot-starter) -[![fork](https://img.shields.io/github/forks/wb04307201/file-preview-spring-boot-starter)](https://github.com/wb04307201/file-preview-spring-boot-starter) +[![fork](https://img.shields.io/github/forks/wb04307201/file-preview-spring-boot-starter)](https://github.com/wb04307201/file-preview-spring-boot-starter) +![MIT](https://img.shields.io/badge/License-Apache2.0-blue.svg) ![JDK](https://img.shields.io/badge/JDK-17+-green.svg) ![SpringBoot](https://img.shields.io/badge/Srping%20Boot-3+-green.svg) +## 简介 > 一个文档在线预览的中间件 > 可通过简单的配置即可集成到springboot中 > 支持word、excel、ppt、pdf、ofd、图片、视频、音频、markdown、代码、网页、epub电子书、Xmind脑图、压缩文件、bpmn(业务流程管理和符号)、cmmn(案例管理模型和符号)、dmn(决策管理和符号)等格式文件的在线预览 + +## 支持的文件类型以及相关的的开源项目 +| 文件类型 | 预览组件 | 预览示例 | +|----------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| word/excel/ppt | [jodconverter](https://github.com/sbraconnier/jodconverter/) | | +| word/excel/ppt | [Spire.Office](https://www.e-iceblue.com/) | | +| word/excel/ppt | [onlyoffice](https://www.onlyoffice.com/zh/) | | +| word/excel/ppt | [libreoffice online](https://zh-cn.libreoffice.org/download/libreoffice-online/) | | +| word/excel/ppt | [Collabora Online](https://www.collaboraoffice.com/) | | +| pdf | [PDF.js](https://mozilla.github.io/pdf.js/) | | +| audio音频 | [audio.js](http://kolber.github.io/audiojs/) | | +| video视频 | [videojs](https://videojs.com/) | | +| markdonw | [vditor](https://github.com/Vanessa219/vditor) | | +| 代码 | [CodeMirror](https://codemirror.net/) | | +| epub电子书 | [epub.js](https://github.com/futurepress/epub.js) | | +| xmid脑图 | [xmind-embed-viewer](https://github.com/xmindltd/xmind-embed-viewer) | | +| 网页 | 直接渲染 | | +| 压缩文件 | [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/) | | +| bpmn | [bpmn.io](https://bpmn.io/) | | +| cmmn | [bpmn.io](https://bpmn.io/) | | +| dmn | [bpmn.io](https://bpmn.io/) | | +| ofd | [ofd.js](https://gitee.com/Donal/ofd.js) | | + + ## 代码示例 1. 使用[文档在线预览](https://gitee.com/wb04307201/file-preview-spring-boot-starter)、[多平台文件存储](https://gitee.com/wb04307201/file-storage-spring-boot-starter)、[实体SQL工具](https://gitee.com/wb04307201/sql-util)实现的[文件预览Demo](https://gitee.com/wb04307201/file-preview-demo) 2. 使用[文档在线预览](https://gitee.com/wb04307201/file-preview-spring-boot-starter)、[多平台文件存储](https://gitee.com/wb04307201/file-storage-spring-boot-starter)、[实体SQL工具](https://gitee.com/wb04307201/sql-util)实现的[文件预览VUE Demo](https://gitee.com/wb04307201/file-preview-vue) -## 第一步 增加 JitPack 仓库 + +## 快速开始 +### 引入依赖 +增加 JitPack 仓库 ```xml @@ -24,7 +53,6 @@ ``` -## 第二步 引入jar 1.1.7版本后groupId更换为com.github.wb04307201 1.2.0版本后升级到jdk17 SpringBoot3+ 继续使用jdk 8请查看jdk8分支 @@ -32,11 +60,11 @@ com.github.wb04307201 file-preview-spring-boot-starter - 1.2.8 + 1.2.9 ``` -## 第三步 在启动类上加上`@EnableFilePreview`注解 +### 在启动类上加上`@EnableFilePreview`注解 ```java @EnableFilePreview @SpringBootApplication @@ -49,7 +77,7 @@ public class FilePreviewDemoApplication { } ``` -## 第四步 添加相关配置 +### 添加相关配置 ```yaml file: preview: @@ -169,12 +197,13 @@ file: storageDomain: http://ip:port #当前服务的域名,用于Collabora Online从当前服务下载文件 ``` -## 第五步 访问内置界面使用文件上传 +### 访问内置界面使用文件上传 上传的文件可通过http://ip:端口/file/preview/list进行查看 注意:如配置了context-path需要在地址中对应添加 ![img_9.png](img_9.png) -## 其他1:实际使用中,可通过配置和实现文件预览记录接口方法将数据持久化到数据库中 +## 高级 +### 如何通过配置和实现文件预览记录接口方法将数据持久化到数据库中 ```yaml file: preview: @@ -228,7 +257,7 @@ public class H2FilePriviewRecordImpl implements IFilePreviewRecord { } ``` -## 其他2:实际使用中,可通过配置和实现文件存储记录接口方法将文件持久化到其他平台中 +### 如何通过配置和实现文件存储记录接口方法将文件持久化到其他平台中 ```yaml file: preview: @@ -271,7 +300,7 @@ public class MinIOFileStorageImpl implements IFileStorage { ``` *注意: 文件存储这部分使用了[file-storage-spring-boot-starter](https://gitee.com/wb04307201/file-storage-spring-boot-starter)* -## 其他3:通过内置Rest接口实现自定义页面 +### 如何通过内置Rest接口实现自定义页面 ```html @@ -435,7 +464,7 @@ public class MinIOFileStorageImpl implements IFileStorage { ``` -## 其他4:通过注入FileStorageService实现自定义Rest接口和自定义页面 +### 如何通过注入FileStorageService实现自定义Rest接口和自定义页面 ```java @Controller public class Demo2Controller { @@ -623,31 +652,7 @@ public class Demo2Controller { ``` -## 其他5:预览文件使用的开源项目 - -| 文件类型 | 预览组件 | 预览示例 | -|----------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| word/excel/ppt | [jodconverter](https://github.com/sbraconnier/jodconverter/) | | -| word/excel/ppt | [Spire.Office](https://www.e-iceblue.com/) | | -| word/excel/ppt | [onlyoffice](https://www.onlyoffice.com/zh/) | | -| word/excel/ppt | [libreoffice online](https://zh-cn.libreoffice.org/download/libreoffice-online/) | | -| word/excel/ppt | [Collabora Online](https://www.collaboraoffice.com/) | | -| pdf | [PDF.js](https://mozilla.github.io/pdf.js/) | | -| audio音频 | [audio.js](http://kolber.github.io/audiojs/) | | -| video视频 | [videojs](https://videojs.com/) | | -| markdonw | [vditor](https://github.com/Vanessa219/vditor) | | -| 代码 | [CodeMirror](https://codemirror.net/) | | -| epub电子书 | [epub.js](https://github.com/futurepress/epub.js) | | -| xmid脑图 | [xmind-embed-viewer](https://github.com/xmindltd/xmind-embed-viewer) | | -| 网页 | 直接渲染 | | -| 压缩文件 | [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/) | | -| bpmn | [bpmn.io](https://bpmn.io/) | | -| cmmn | [bpmn.io](https://bpmn.io/) | | -| dmn | [bpmn.io](https://bpmn.io/) | | -| ofd | [ofd.js](https://gitee.com/Donal/ofd.js) | | - - -## 其他6:自定义预览界面渲染 +### 如何自定义文件预览方式 比如在实际使用minio作为对象存储,并想直接使用minio的url播放上传的视频 可通过继承IRenderPage并实现support和render方法的方式自定义页面渲染的方式 ```java diff --git a/pom.xml b/pom.xml index 622bf5780383a46425c15fa7442f272dc5178620..87dde7cdfffed5849cf50b755f4e0271e75e4577 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,13 @@ fusionauth-jwt 5.3.2 + + + + jakarta.transaction + jakarta.transaction-api + 2.0.1 + @@ -120,6 +127,11 @@ fusionauth-jwt + + jakarta.transaction + jakarta.transaction-api + + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/cn/wubo/file/preview/config/CollaboraProperties.java b/src/main/java/cn/wubo/file/preview/config/CollaboraProperties.java deleted file mode 100644 index dabdfcb1e64d4bbae8d7a4aac7aefefa1b917691..0000000000000000000000000000000000000000 --- a/src/main/java/cn/wubo/file/preview/config/CollaboraProperties.java +++ /dev/null @@ -1,9 +0,0 @@ -package cn.wubo.file.preview.config; - -import lombok.Data; - -@Data -public class CollaboraProperties { - private String domain; - private String storageDomain; -} diff --git a/src/main/java/cn/wubo/file/preview/config/FilePreviewProperties.java b/src/main/java/cn/wubo/file/preview/config/FilePreviewProperties.java index c0180f3edc422d478452969b830b298b673f2921..668c026f3916c12afb53d72550359768052b11f6 100644 --- a/src/main/java/cn/wubo/file/preview/config/FilePreviewProperties.java +++ b/src/main/java/cn/wubo/file/preview/config/FilePreviewProperties.java @@ -14,4 +14,24 @@ public class FilePreviewProperties { private OnlyOfficeProperties onlyOffice = new OnlyOfficeProperties(); private LibreOfficeProperties libreOffice = new LibreOfficeProperties(); private CollaboraProperties collabora = new CollaboraProperties(); + + @Data + public static class CollaboraProperties { + private String domain; + private String storageDomain; + } + + @Data + public static class LibreOfficeProperties { + private String domain; + private String storage; + } + + @Data + public static class OnlyOfficeProperties { + private String domain; + private String download; + private String callback; + private String secret; + } } diff --git a/src/main/java/cn/wubo/file/preview/config/LibreOfficeProperties.java b/src/main/java/cn/wubo/file/preview/config/LibreOfficeProperties.java deleted file mode 100644 index 0adcde3e5d027c738bcd871b73d0fd83c92e10e9..0000000000000000000000000000000000000000 --- a/src/main/java/cn/wubo/file/preview/config/LibreOfficeProperties.java +++ /dev/null @@ -1,9 +0,0 @@ -package cn.wubo.file.preview.config; - -import lombok.Data; - -@Data -public class LibreOfficeProperties { - private String domain; - private String storage; -} diff --git a/src/main/java/cn/wubo/file/preview/config/OfficeConfiguration.java b/src/main/java/cn/wubo/file/preview/config/OfficeConfiguration.java index 1136a5a7960dc0fbdf78c1970ee12077ae5f99aa..eb79a5fd4a889c9070b7ddad1e5633b7b182593d 100644 --- a/src/main/java/cn/wubo/file/preview/config/OfficeConfiguration.java +++ b/src/main/java/cn/wubo/file/preview/config/OfficeConfiguration.java @@ -97,13 +97,21 @@ public class OfficeConfiguration { */ @Bean public FilePreviewService filePreviewService(IOfficeConverter officeConverter, List fileStorageList, List filePreviewRecordList) { + // @formatter:off // 根据配置选择并初始化文件存储服务 - IFileStorage fileStorage = fileStorageList.stream().filter(obj -> obj.getClass().getName().equals(properties.getFileStorage())).findAny().orElseThrow(() -> new StorageRuntimeException(String.format("未找到%s对应的bean,无法加载IFileStorage!", properties.getFileStorage()))); + IFileStorage fileStorage = fileStorageList.stream() + .filter(obj -> obj.getClass().getName().equals(properties.getFileStorage())) + .findAny() + .orElseThrow(() -> new StorageRuntimeException(String.format("未找到%s对应的bean,无法加载IFileStorage!", properties.getFileStorage()))); fileStorage.init(); // 根据配置选择并初始化文件预览记录服务 - IFilePreviewRecord filePreviewRecord = filePreviewRecordList.stream().filter(obj -> obj.getClass().getName().equals(properties.getFilePreviewRecord())).findAny().orElseThrow(() -> new RecordRuntimeException(String.format("未找到%s对应的bean,无法加载IFilePreviewRecord!", properties.getFilePreviewRecord()))); + IFilePreviewRecord filePreviewRecord = filePreviewRecordList.stream() + .filter(obj -> obj.getClass().getName().equals(properties.getFilePreviewRecord())) + .findAny() + .orElseThrow(() -> new RecordRuntimeException(String.format("未找到%s对应的bean,无法加载IFilePreviewRecord!", properties.getFilePreviewRecord()))); filePreviewRecord.init(); + // @formatter:on // 创建并返回文件预览服务实例 return new FilePreviewService(officeConverter, fileStorage, filePreviewRecord); @@ -217,6 +225,7 @@ public class OfficeConfiguration { String id = request.pathVariable("id"); // 从请求路径中获取文件id FilePreviewInfo info = filePreviewService.findById(id); // 根据id查询文件预览信息 byte[] bytes = filePreviewService.getBytes(info); // 根据文件信息获取文件内容 + // @formatter:off // 构建返回文件内容的响应 return ServerResponse.ok().contentType(MediaType.parseMediaType(FileUtils.getMimeType(info.getFileName()))) // 根据文件名设置响应的内容类型 .contentLength(bytes.length) // 设置响应内容的长度 @@ -229,6 +238,7 @@ public class OfficeConfiguration { } return new ModelAndView(); // 返回空的ModelAndView对象 }); + // @formatter:on }); } @@ -272,8 +282,12 @@ public class OfficeConfiguration { String id = request.param("id").orElseThrow(() -> new IllegalArgumentException(LOST_ID)); FilePreviewInfo info = filePreviewService.findById(id); byte[] bytes = filePreviewService.getBytes(info); + // @formatter:off // 处理文件下载请求,返回文件内容。 - return ServerResponse.ok().contentType(MediaType.parseMediaType(FileUtils.getMimeType(info.getFileName()))).contentLength(bytes.length).header("Content-Disposition", "attachment;filename=" + new String(Objects.requireNonNull(info.getFileName()).getBytes(), StandardCharsets.ISO_8859_1)).build((res, req) -> { + return ServerResponse.ok().contentType(MediaType.parseMediaType(FileUtils.getMimeType(info.getFileName()))) + .contentLength(bytes.length) + .header("Content-Disposition", "attachment;filename=" + new String(Objects.requireNonNull(info.getFileName()).getBytes(), StandardCharsets.ISO_8859_1)) + .build((res, req) -> { try (OutputStream os = req.getOutputStream()) { IoUtils.writeToStream(bytes, os); } catch (IOException e) { @@ -281,6 +295,7 @@ public class OfficeConfiguration { } return null; }); + // @formatter:on }); } diff --git a/src/main/java/cn/wubo/file/preview/config/OnlyOfficeProperties.java b/src/main/java/cn/wubo/file/preview/config/OnlyOfficeProperties.java deleted file mode 100644 index cdfb941925704a0b42ab17cbe7809ce83afce336..0000000000000000000000000000000000000000 --- a/src/main/java/cn/wubo/file/preview/config/OnlyOfficeProperties.java +++ /dev/null @@ -1,11 +0,0 @@ -package cn.wubo.file.preview.config; - -import lombok.Data; - -@Data -public class OnlyOfficeProperties { - private String domain; - private String download; - private String callback; - private String secret; -} diff --git a/src/main/java/cn/wubo/file/preview/core/FilePreviewService.java b/src/main/java/cn/wubo/file/preview/core/FilePreviewService.java index 6f5b56889f332f38c7010d05aa8b2353700bc45c..61704dc9fb5c11e189db6ffcef33fd9ef0b77a12 100644 --- a/src/main/java/cn/wubo/file/preview/core/FilePreviewService.java +++ b/src/main/java/cn/wubo/file/preview/core/FilePreviewService.java @@ -6,6 +6,7 @@ import cn.wubo.file.preview.record.IFilePreviewRecord; import cn.wubo.file.preview.storage.IFileStorage; import cn.wubo.file.preview.utils.FileUtils; import cn.wubo.file.preview.utils.IoUtils; +import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import org.apache.commons.compress.archivers.ArchiveException; import org.springframework.util.FastByteArrayOutputStream; @@ -43,6 +44,7 @@ public class FilePreviewService { * @param fileName 原文件名。 * @return 返回包含转换后文件名、文件路径、原文件名和创建时间的文件预览信息对象。 */ + @Transactional(rollbackOn = Exception.class) public FilePreviewInfo covert(InputStream is, String fileName) { // 用于存储转换后的文件字节内容 byte[] bytes; @@ -88,6 +90,7 @@ public class FilePreviewService { * @param id 预览信息的唯一标识符。 * @return 返回一个布尔值,表示是否成功删除。若文件预览信息和对应的文件都成功删除,则返回true;否则返回false。 */ + @Transactional(rollbackOn = Exception.class) public Boolean delete(String id) { // 根据ID查找文件预览信息 FilePreviewInfo filePreviewInfo = filePreviewRecord.findById(id); diff --git a/src/main/java/cn/wubo/file/preview/page/PageFactory.java b/src/main/java/cn/wubo/file/preview/page/PageFactory.java index 411d1231b71401f9a3f5219aefbf2f962dbc8de8..bea9050a1837f87eb08c2ef208b66058b9e0f3f8 100644 --- a/src/main/java/cn/wubo/file/preview/page/PageFactory.java +++ b/src/main/java/cn/wubo/file/preview/page/PageFactory.java @@ -16,12 +16,30 @@ public class PageFactory { private static final Set OFFICE_FILE_TYPES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("word", "excel", "power point", "text"))); + private PageFactory() { + } + + /** + * 根据文件预览信息、文件预览服务、文件预览属性和上下文路径创建抽象页面。 + * 此方法根据文件类型和配置的办公文档转换器类型,动态决定创建哪种类型的页面。 + * + * @param info 文件预览信息,包含文件名等信息。 + * @param filePreviewService 文件预览服务,用于提供文件预览的功能支持。 + * @param properties 文件预览属性,包含配置信息,如办公文档转换器类型。 + * @param contextPath 上下文路径,用于构造页面的URL。 + * @return 创建的抽象页面实例。 + * @throws PageRuntimeException 如果页面创建过程中发生异常。 + */ public static AbstractPage create(FilePreviewInfo info, FilePreviewService filePreviewService, FilePreviewProperties properties, String contextPath) { try { + // 获取文件的扩展名 String extName = FileUtils.extName(info.getFileName()); + // 根据扩展名确定文件类型 String fileType = FileUtils.fileType(extName); Class clazz; + + // 根据配置的办公文档转换器类型和文件类型,决定创建的页面类 if ("only".equals(properties.getOfficeConverter()) && OFFICE_FILE_TYPES.contains(fileType)) { clazz = PageType.getClass("only"); } else if ("lool".equals(properties.getOfficeConverter()) && OFFICE_FILE_TYPES.contains(fileType)) { @@ -31,13 +49,14 @@ public class PageFactory { } else { clazz = PageType.getClass(fileType); } - return clazz.getConstructor(String.class, String.class, String.class, FilePreviewInfo.class, FilePreviewService.class, FilePreviewProperties.class).newInstance(fileType, extName, contextPath, info, filePreviewService, properties); + + // 通过反射创建页面类的实例,并传递必要的参数 + return clazz.getConstructor(String.class, String.class, String.class, FilePreviewInfo.class, FilePreviewService.class, FilePreviewProperties.class) + .newInstance(fileType, extName, contextPath, info, filePreviewService, properties); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + // 如果在创建页面过程中发生异常,抛出自定义的运行时异常 throw new PageRuntimeException(e.getMessage(), e); } } - - private PageFactory() { - } } diff --git a/src/main/java/cn/wubo/file/preview/record/impl/MemFilePreviewRecordImpl.java b/src/main/java/cn/wubo/file/preview/record/impl/MemFilePreviewRecordImpl.java index f6691935f1f926574f414d0fc2afc4283f536635..03b59b13ee2b1a4f3aa8bd311ec69fd7c2df942a 100644 --- a/src/main/java/cn/wubo/file/preview/record/impl/MemFilePreviewRecordImpl.java +++ b/src/main/java/cn/wubo/file/preview/record/impl/MemFilePreviewRecordImpl.java @@ -27,7 +27,13 @@ public class MemFilePreviewRecordImpl implements IFilePreviewRecord { @Override public List list(FilePreviewInfo filePreviewInfo) { - return filePreviewInfos.stream().filter(e -> !StringUtils.hasLength(filePreviewInfo.getId()) || e.getId().equals(filePreviewInfo.getId())).filter(e -> !StringUtils.hasLength(filePreviewInfo.getFileName()) || e.getFileName().contains(filePreviewInfo.getFileName())).filter(e -> !StringUtils.hasLength(filePreviewInfo.getFilePath()) || e.getFilePath().contains(filePreviewInfo.getFilePath())).toList(); + // @formatter:off + return filePreviewInfos.stream() + .filter(e -> !StringUtils.hasLength(filePreviewInfo.getId()) || e.getId().equals(filePreviewInfo.getId())) + .filter(e -> !StringUtils.hasLength(filePreviewInfo.getFileName()) || e.getFileName().contains(filePreviewInfo.getFileName())) + .filter(e -> !StringUtils.hasLength(filePreviewInfo.getFilePath()) || e.getFilePath().contains(filePreviewInfo.getFilePath())) + .toList(); + // @formatter:om } @Override diff --git a/src/main/java/cn/wubo/file/preview/utils/IoUtils.java b/src/main/java/cn/wubo/file/preview/utils/IoUtils.java index 51791260e88143b911c88f830f4b23a7384530d0..46514192b44ce084064a114cf9af5d2e6cf5efe5 100644 --- a/src/main/java/cn/wubo/file/preview/utils/IoUtils.java +++ b/src/main/java/cn/wubo/file/preview/utils/IoUtils.java @@ -101,10 +101,8 @@ public class IoUtils { * @throws IOException 如果在读取文件或写入文件过程中发生I/O错误。 */ public static String readLines(byte[] bytes, String fileName) throws IOException { - // 创建一个临时文件 - Path path = Files.createTempFile(String.valueOf(System.currentTimeMillis()), fileName); - // 将字节数据写入临时文件 - Files.write(path, bytes); + // 创建一个临时文件,并将字节数据写入临时文件 + Path path = FileUtils.writeTempFile(fileName, bytes); try (Stream lines = Files.lines(path)) { // 读取临时文件的每行内容,合并为一个字符串,并对其进行Base64编码 return new String(Base64.getEncoder().encode(lines.collect(Collectors.joining("\n")).getBytes())); diff --git a/src/main/java/cn/wubo/file/preview/utils/PageUtils.java b/src/main/java/cn/wubo/file/preview/utils/PageUtils.java index 6ce0d51c6c9019188de074707b728181de7c706e..117f04d5ebcd072c7188fb98d10170d9c168c75c 100644 --- a/src/main/java/cn/wubo/file/preview/utils/PageUtils.java +++ b/src/main/java/cn/wubo/file/preview/utils/PageUtils.java @@ -1,6 +1,7 @@ package cn.wubo.file.preview.utils; import cn.wubo.file.preview.exception.PageRuntimeException; +import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; @@ -15,7 +16,7 @@ public class PageUtils { public static String write(String templateName, Map params) { try (StringWriter sw = new StringWriter()) { - freemarker.template.Configuration cfg = new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_23); + Configuration cfg = new Configuration(freemarker.template.Configuration.VERSION_2_3_23); cfg.setClassForTemplateLoading(PageUtils.class, "/template"); Template template = cfg.getTemplate(templateName, "UTF-8"); template.process(params, sw);