Java:封装POI实现word的docx文件的简单模板功能
2016-05-06 13:51
946 查看
一:场景
通过Word模板来实现动态的word生成
二: 基本要求
1:替换文本中的内容
2:替换表格中的内容(不用动态生成表格)
3:替换后的内容应该与替换前的内容格式相同
4:模板修改方便
5:效果如下:
模板:
结果:
三:poi分析
使用方法:直接读取word文件,替换里面各个部分的内容
优点:直接使用word文件作为模板
缺点:本身的替换逻辑无法保留格式
四:为什么选择封装POI
1:因为时间和学习成本(懒)的问题,没有研究docx的xml规则,因此决定直接对现有的工具进行封装,来实现需求.
2:freeMarker本身只是对通用的模板进行处理,底层并不能直接解析word文件.
而poi本身就是对word文件进行操作的,因此可以对直接在poi的api上进一步的封装.
五:可行性分析
1.POI使用XWPFDocument对象来解析docx的文件,通过在构造时传入docx文件的读取流完成解析过程,将解析后将信息分门别类的存储在不同的对象中.并提供write(OutputStream stream)方法将这些对象重新转换为xml写入到文件中.
2.XWPFDocument中的对象存储有文本和格式等信息,能够拿到或修改这些信息
3.结合上述1,2两点,表示了POI存在修改word文本并保留格式的可能,通过编写Demo修改了XWPFDocument中的一个XWPFParagraph对象中的XWPFRun文本内容后证明了这一点.
六: 实现原理
1.文本内容和表格内容分别可以通过 getParagraphs() 和 getTables() 两个方法得到
2.查看XWPFTable发现,最后表格中的文本内容还是用XWPFParagraph对象存储
因此很可能最后修改表格和文本最后都汇集到一个相同的地方,就是修改XWPFParagraph的文本内容.进过简单的测试,证明了无论是表格还是文本,其内容都可以通过XWPFParagraph进行修改.
3.研究XWPFParagraph中的内容发现,每个XWPFParagraph对象中存储的都是一整段的内容,而这一整段的内容被分割成多个部分存入了XWPFParagraph对象中的集合List runs,也许因为docx文件转换成xml的时候很大几率的会将相同格式的文本分割开来,所以每个XWPFRun展示的内容都是不连续的!!而这正是实现word模板的难点所在.
4.因为没打算再深入研究docx转xml的原理,因此打算在现有的不连续的基础上进行封装,将其组合起来,模拟成”连续”的对象,最后对”连续”的对象进行replace操作.我的实现方法如下
(1)获得完整的文本内容,用于判断是否包含${key}
(2)对文本内容的每个字符标记所属的XWPFRun对象
(3)如果匹配,则从标记中获取匹配的第一个字符,得到字符对应的标记对象,将替换的内容全部标记为该对象
(4)替换完成后,遍历所有的标记,将标记对象所属的字符重新组合成String后重新设置,并将无用的标记对象文本设置为空
5.最后只需调用XWPFDocument.write(OutputStream)方法即可得到基于模板生成的docx文件了.
七:小结
完整的代码我已上传到GitHub,里面包含了说明以及测试用例,可以作为参考.
https://github.com/DeveloperHuang/WordHandler
目前只是简单的实现了替换的功能,后续功能可能下半年才有空研究了.
通过Word模板来实现动态的word生成
二: 基本要求
1:替换文本中的内容
2:替换表格中的内容(不用动态生成表格)
3:替换后的内容应该与替换前的内容格式相同
4:模板修改方便
5:效果如下:
模板:
结果:
三:poi分析
使用方法:直接读取word文件,替换里面各个部分的内容
优点:直接使用word文件作为模板
缺点:本身的替换逻辑无法保留格式
四:为什么选择封装POI
1:因为时间和学习成本(懒)的问题,没有研究docx的xml规则,因此决定直接对现有的工具进行封装,来实现需求.
2:freeMarker本身只是对通用的模板进行处理,底层并不能直接解析word文件.
而poi本身就是对word文件进行操作的,因此可以对直接在poi的api上进一步的封装.
五:可行性分析
1.POI使用XWPFDocument对象来解析docx的文件,通过在构造时传入docx文件的读取流完成解析过程,将解析后将信息分门别类的存储在不同的对象中.并提供write(OutputStream stream)方法将这些对象重新转换为xml写入到文件中.
2.XWPFDocument中的对象存储有文本和格式等信息,能够拿到或修改这些信息
3.结合上述1,2两点,表示了POI存在修改word文本并保留格式的可能,通过编写Demo修改了XWPFDocument中的一个XWPFParagraph对象中的XWPFRun文本内容后证明了这一点.
XWPFDocument docx = new XWPFDocument(InputStream); XWPFRun run = docx.getParagraphs().get(0).getRuns().get(0); run.setText("modify"); docx.write(OutputStream);
六: 实现原理
1.文本内容和表格内容分别可以通过 getParagraphs() 和 getTables() 两个方法得到
XWPFDocument docx = new XWPFDocument(InputStream); //文本内容 List<XWPFParagraph> allXWPFParagraphs = docx.getParagraphs(); //表格内容 List<XWPFTable> xwpfTables = docx.getTables();
2.查看XWPFTable发现,最后表格中的文本内容还是用XWPFParagraph对象存储
//获得每行 List<XWPFTableRow> xwpfTableRows = = xwpfTable.getRows(); List<XWPFTableCell> xwpfTableCells = new ArrayList<XWPFTableCell>(); for(int i = 0 ; i < xwpfTableRows.size() ; i++){ //获得一行的所有单元 xwpfTableCells.addAll(getCells(i)); } List<XWPFParagraph> xwpfParagraphs = new ArrayList<XWPFParagraph>(); for(XWPFTableCell cell : xwpfTableCells){ //获得每个单元中的文本 xwpfParagraphs.addAll(cell.getParagraphs()); }
因此很可能最后修改表格和文本最后都汇集到一个相同的地方,就是修改XWPFParagraph的文本内容.进过简单的测试,证明了无论是表格还是文本,其内容都可以通过XWPFParagraph进行修改.
3.研究XWPFParagraph中的内容发现,每个XWPFParagraph对象中存储的都是一整段的内容,而这一整段的内容被分割成多个部分存入了XWPFParagraph对象中的集合List runs,也许因为docx文件转换成xml的时候很大几率的会将相同格式的文本分割开来,所以每个XWPFRun展示的内容都是不连续的!!而这正是实现word模板的难点所在.
4.因为没打算再深入研究docx转xml的原理,因此打算在现有的不连续的基础上进行封装,将其组合起来,模拟成”连续”的对象,最后对”连续”的对象进行replace操作.我的实现方法如下
(1)获得完整的文本内容,用于判断是否包含${key}
(2)对文本内容的每个字符标记所属的XWPFRun对象
(3)如果匹配,则从标记中获取匹配的第一个字符,得到字符对应的标记对象,将替换的内容全部标记为该对象
(4)替换完成后,遍历所有的标记,将标记对象所属的字符重新组合成String后重新设置,并将无用的标记对象文本设置为空
public class XWPFParagraphUtils { private XWPFParagraph paragraph; private List<XWPFRun> allXWPFRuns; //所有run的String合并后的内容 private StringBuffer context ; //长度与context对应的RunChar集合 List<RunChar> runChars ; public XWPFParagraphUtils(XWPFParagraph paragraph){ this.paragraph = paragraph; initParameter(); } /** * 初始化各参数 */ private void initParameter(){ context = new StringBuffer(); runChars = new ArrayList<XWPFParagraphUtils.RunChar>(); allXWPFRuns = new ArrayList<XWPFRun>(); setXWPFRun(); } /** * 设置XWPFRun相关的参数 * @param run * @throws Exception */ private void setXWPFRun() { allXWPFRuns = paragraph.getRuns(); if(allXWPFRuns == null || allXWPFRuns.size() == 0){ return; }else{ for (XWPFRun run : allXWPFRuns) { int testPosition = run.getTextPosition(); String text = run.getText(testPosition); if(text == null || text.length() == 0){ return; } this.context.append(text); for(int i = 0 ; i < text.length() ; i++){ runChars.add(new RunChar(text.charAt(i), run)); } } } System.out.println(context.toString()); } public String getString(){ return context.toString(); } public boolean contains(String key){ return context.indexOf(key) >= 0 ? true : false; } /** * 所有匹配的值替换为对应的值 * @param key(匹配模板中的${key}) * @param value 替换后的值 * @return */ public boolean replaceAll(String key,String value){ boolean replaceSuccess = false; key = "${" + key + "}"; while(replace(key, value)){ replaceSuccess = true; } return replaceSuccess; } /** * 所有匹配的值替换为对应的值(key匹配模板中的${key}) * @param param 要替换的key-value集合 * @return */ public boolean replaceAll(Map<String,String> param){ Set<Entry<String, String>> entrys = param.entrySet(); boolean replaceSuccess = false; for (Entry<String, String> entry : entrys) { String key = entry.getKey(); boolean currSuccessReplace = replaceAll(key,entry.getValue()); replaceSuccess = replaceSuccess?replaceSuccess:currSuccessReplace; } return replaceSuccess; } /** * 将第一个匹配到的值替換为对应的值 * @param key * @param value * @return */ private boolean replace(String key,String value){ if(contains(key)){ /* * 1:得带key对应的开始和结束下标 */ int startIndex = context.indexOf(key); int endIndex = startIndex+key.length(); /* * 2:获取第一个匹配的XWPFRun */ RunChar startRunChar = runChars.get(startIndex); XWPFRun startRun = startRunChar.getRun(); /* * 3:将匹配的key清空 */ runChars.subList(startIndex, endIndex).clear(); /* * 4:将value设置到startRun中 */ List<RunChar> addRunChar = new ArrayList<XWPFParagraphUtils.RunChar>(); for(int i = 0 ; i < value.length() ; i++){ addRunChar.add(new RunChar(value.charAt(i), startRun)); } runChars.addAll(startIndex, addRunChar); resetRunContext(runChars); return true; }else{ return false; } } private void resetRunContext(List<RunChar> newRunChars){ /** * 生成新的XWPFRun与Context的对应关系 */ HashMap<XWPFRun, StringBuffer> newRunContext = new HashMap<XWPFRun, StringBuffer>(); //重设context context = new StringBuffer(); for(RunChar runChar : newRunChars){ StringBuffer newRunText ; if(newRunContext.containsKey(runChar.getRun())){ newRunText = newRunContext.get(runChar.getRun()); }else{ newRunText = new StringBuffer(); } context.append(runChar.getValue()); newRunText.append(runChar.getValue()); newRunContext.put(runChar.getRun(), newRunText); } /** * 遍历旧的runContext,替换context * 并重新设置run的text,如果不匹配,text设置为"" */ for(XWPFRun run : allXWPFRuns){ if(newRunContext.containsKey(run)){ String newContext = newRunContext.get(run).toString(); XWPFRunUtils.setText(run,newContext); }else{ XWPFRunUtils.setText(run,""); } } } /** * 实体类:存储字节与XWPFRun对象的对应关系 * @author JianQiu */ class RunChar{ /** * 字节 */ private char value; /** * 对应的XWPFRun */ private XWPFRun run; public RunChar(char value,XWPFRun run){ this.setValue(value); this.setRun(run); } public char getValue() { return value; } public void setValue(char value) { this.value = value; } public XWPFRun getRun() { return run; } public void setRun(XWPFRun run) { this.run = run; } } }
5.最后只需调用XWPFDocument.write(OutputStream)方法即可得到基于模板生成的docx文件了.
七:小结
完整的代码我已上传到GitHub,里面包含了说明以及测试用例,可以作为参考.
https://github.com/DeveloperHuang/WordHandler
目前只是简单的实现了替换的功能,后续功能可能下半年才有空研究了.
相关文章推荐
- 如何在 Eclipse 中使用命令行
- java NIO详解
- 【Java】使用CSVUtils生成文件并供下载
- Java:使用Executors创建和管理线程
- java初学者:接口技术文章
- JAVA自学之路1-JDK安装及配置
- 用Jersey构建RESTful服务-JAVA对象转成XML/Json输出
- 导入eclipse项目出错Gradle DSL method not found: 'android()'
- 【Java】数据库查询的数据直接以指定文件类型下载到本地(弹出下载框)
- java.lang.String.indexOf()方法
- 4、Ajax与Java交互的案例
- springmvc错误java.lang.IllegalArgumentException
- Java线程专栏文章汇总
- spring-data-jpa 中,如果使用了one-to-many , many-to-one的注释,会在Jackson进行json字符串化的时候出现错误
- 3、在Ajax于Java的交互过程中,加入加载图片
- Spring框架的搭建
- 常见快捷键eclipse
- spring 学习
- 2、Ajax与Java通过POST方式交互
- java底层知识(4)--伪共享