java实现文件对比

简介: 基于java实现类似于svn的文件对比功能及效果,该对比适用于html,js,css,text等

需求

web项目需要实现文件内容对比功能,开发语言是java,也就是通过java实现类似于svn的文件对比功能

实现效果

效果图如下

image.png

后端代码引入

首先引入对比的核心jar包

<!--对比工具依赖-->
        <dependency>
            <groupId>io.github.java-diff-utils</groupId>
            <artifactId>java-diff-utils</artifactId>
            <version>4.9</version>
        </dependency>

页面流程实现,点击列表页按钮  历史版本  查看历史版本记录

image.png

历史版本跳转方法

<li>
  <a class="btn btn-info" onclick="getHistoryVersion();" shiro:hasPermission="project:publishCosFiles:history">
    <i class="fa fa-history"></i> 历史版本
  </a>
</li>
//查看当前目录的历史版本
function getHistoryVersion(){
  $.modal.openTab('历史版本',prefix + "/getHistoryVersion?platform="+$('#platform').val()+"&title="+$("#title").val());
}

controller方法,接收页面传参跳转到历史版本列表页面

/**
 * 获取目录历史版本页面
 */
@RequestMapping("/getHistoryVersion")
@RequiresPermissions("project:publishCosFiles:history")
public String getHistoryVersion(HttpServletRequest request) throws Exception {
  String title = request.getParameter("title");
  String platform = request.getParameter("platform");
  request.setAttribute("title", title);
  request.setAttribute("platform", platform);
  return prefix + "/publishCosFileVersion";
}

历史版本列表页面

image.png

列表页面代码 publishCosFileVersion.html

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
  <th:block th:include="include :: header('文件版本列表')" />
  <style>
    .my-search-collapse {
      width: 100%;
      background: #fff;
      border-radius: 6px;
      margin-top: 10px;
      padding-top: 10px;
      padding-bottom: 6px;
      box-shadow: 1px 1px 3px rgba(0,0,0,.2);
    }
  </style>
</head>
<body class="gray-bg">
<div class="container-div">
  <div class="row">
    <div class="col-sm-12 my-search-collapse">
      <div>
        <div style="color:red;">当前目录:[[${title}]]</div>
      </div>
    </div>
    <div class="col-sm-12 search-collapse">
      <form id="formId">
        <div class="select-list">
          <ul>
            <input type="hidden" id="path" name="path" th:value="${title}">
            <input type="hidden" id="platform" name="platform" th:value="${platform}">
            <li>
              文件名:<input type="text" name="name"/>
            </li>
            <li>
              <input name="createUser" type="hidden" id="createUser"/>
              操作人:<input id="createUserName" name="createUserName" readonly  type="text" onclick="selectUserDeptTree('createUser','createUserName')" >
            </li>
            <li>
              备注信息:<input type="text" name="remark"/>
            </li>
            <li class="input-daterange input-group">
              修改时间:
              <input type="text" name="createStartDate" readonly id="createStartDate"/>
              <input type="text" name="createEndDate" readonly id="createEndDate"/>
            </li>
            <li>
              <a class="btn btn-primary btn-rounded btn-sm" onclick="search1();"><i
                  class="fa fa-search"></i>&nbsp;搜索</a>
              <a class="btn btn-warning btn-rounded btn-sm" onclick="reset1();"><i
                  class="fa fa-refresh"></i>&nbsp;重置</a>
            </li>
            <li style="float: right">
              <button style="width: 40px;" class="btn btn-default btn-outline" type="button" onclick="search1();" name="refresh" aria-label="刷新" title="刷新"><i class="glyphicon glyphicon-refresh icon-refresh"></i> </button>
            </li>
          </ul>
        </div>
      </form>
    </div>
    <div class="col-sm-12 select-table table-striped">
      <table id="bootstrap-table" data-mobile-responsive="true"></table>
    </div>
  </div>
</div>
<div th:include="include :: footer"></div>
<script th:inline="javascript">
    var viewImgFlag = [[${@permission.hasPermi('project:publishCosFiles:showImage')}]];
    var prefix = ctx + "project/publishFileVersion";
    $(function() {
        var options = {
            url: prefix + "/cmsFileVersionList",
            modalName: "文件版本",
            showExport: false,
            pagination: false,
            showSearch: false,
            showToggle: false,
            showRefresh:false,
            showColumns:false,
            sortName:'name',
            sortOrder:'asc',
            columns: [
                {
                    title: '',
                    align: 'center',
                    formatter: function(value, row, index) {
                        var actions = [];
                        //所有的加版本号
            actions.push('<a class="" href="javascript:" onclick="addChildHtml(\'' + row.name + '\', this,'+row.id+' )"><i class="glyphicon glyphicon-plus icon-plus"></i></a>');
                        return actions.join('');
                    }
                },
                {
                    checkbox: true
                },
                {
                    field : 'id',
                    title : '主键id',
                    visible: false
                },
                {
                    field : 'name',
                    title : '文件名',
          formatter: function(value, row, index) {
            var showname = value;
            if (value.length > 15) {
              showname = value.substr(0,14)+"...";
            }
            var html = "<div title='"+value+"'>"+showname+"</div>";
            return html;
          },
                    sortable:true
                },
                {
                    field : 'versionNum',
                    title : '版本号',
                },
                {
                    field : 'createDate',
                    title : '修改时间',
                    sortable:true
                },
                {
                    field : 'size',
                    title : '文件大小',
                    formatter: function(value, row, index) {
                        if(row.delFlag === 1){
                            //  删除的时候大小展示已删除
                            return "已删除";
                        }
                        var fileSize = row.size;
                        if (fileSize != null) {
                            var number1024 = 1<<10;
                            var number1048576 = 1<<20;
                            var number1073741824 = 1<<30;
                            if (fileSize > number1024) {
                                if (fileSize > number1048576) {
                                    if (fileSize > number1073741824) {
                                        var fixed = (fileSize/number1073741824).toFixed(2);
                                        return fixed+"GB";
                                    }else {
                                        var fixed = (fileSize/number1048576).toFixed(2);
                                        return fixed+"MB";
                                    }
                                }else {
                                    var fixed = (fileSize/number1024).toFixed(2);
                                    return fixed+"KB";
                                }
                            }else {
                                return fileSize+"B";
                            }
                        }else {
                            return "-";
                        }
                    }
                },
                {
                    field : 'createUserName',
                    title : '修改人',
                },
                {
                    field : 'remark',
                    title : '备注信息',
                    formatter: function(value, row, index) {
                        return $.table.tooltip(value);
                    }
                },
                {
                    title: '操作',
                    align: 'left',
                    cellStyle: function (value, row, index) {
                        return {css: {"overflow": "hidden", "text-overflow": "ellipsis", "white-space": "nowrap"}}
                    },
                    formatter: function(value, row, index) {
                        var actions = [];
                        var fileType = row.type;
                        if (fileType != null
                            && ("jpg" == fileType.toLowerCase() || "png" == fileType.toLowerCase() || "gif" == fileType.toLowerCase() || "jpeg" == fileType.toLowerCase())) {
                            actions.push('<a class="btn btn-success btn-xs ' + viewImgFlag + '" href="javascript:void(0)" onclick="viewImage(\'' + row.bakPathPrefix +"/"+ row.bakName +'\')"><i class="fa fa-leaf"></i>预览</a> ');
                        }
                        return actions.join('');
                    }
                }]
        };
        $.table.init(options);
        initLaydate();
    });
    /*账户管理-新增-选择人部门树*/
    function selectUserDeptTree(treeId,treeName) {
        // url 可选参数, 其他参数 请自定扩展
        // permission 权限 0.无权限 1.按照登录人数据权限
        // showType 展示方式 0.人员 1.人员+工号
        // checkType 选中类型 0.多选 1.单选
        // showLevel 展示层级
        var url = ctx + "system/userDeptTree?showType=1&showLevel=2&checkType=1";
        var options = {
            title: '选择部门',
            width: "500",
            url: url,
            callBack: function (index, layero) {
                // 是否允许选父级
                var body = layer.getChildFrame('body', index);
                $("#"+treeId).val(body.find('#ids').val());
                $("#"+treeName).val(body.find('#names').val());
                layer.close(index);
            }
        };
        $.modal.openOptions(options);
    }
    function initLaydate() {
        layui.use('laydate', function() {
            var laydate = layui.laydate;
            var startDate = laydate.render({
                elem: '#createStartDate',
                //max: $('#createEndDate').val(),
                type: 'datetime',
                theme: 'molv',
                trigger: 'click'
                /*done: function (value, date) {
                    // 结束时间大于开始时间
                    if (value !== '') {
                        endDate.config.min.year = date.year;
                        endDate.config.min.month = date.month - 1;
                        endDate.config.min.date = date.date;
                    } else {
                        endDate.config.min.year = '';
                        endDate.config.min.month = '';
                        endDate.config.min.date = '';
                    }
                }*/
            });
            var endDate = laydate.render({
                elem: '#createEndDate',
        //max:$.common.getNowFormatDate(),
                type: 'datetime',
                theme: 'molv',
                trigger: 'click'
                /*done: function (value, date) {
                    // 开始时间小于结束时间
                    if (value !== '') {
                        startDate.config.max.year = date.year;
                        startDate.config.max.month = date.month - 1;
                        startDate.config.max.date = date.date;
                    } else {
                        startDate.config.max.year = '';
                        startDate.config.max.month = '';
                        startDate.config.max.date = '';
                    }
                }*/
            });
        })
    }
    //预览图片
    function viewImage(filePath) {
        var url = ctx + 'project/publishCosFiles/showImage?filePath=' + filePath;
        $.modal.open("图片预览", url , '600', '500',function (index) {
      layer.close(index);
    });
    }
    //加载当前文件的历史版本
    function addChildHtml(name,tdThis,id) {
//      判断是否是加号,加号的话展开并加载子类,如果是减号的话则删除掉子类
        if ($(tdThis).children('i').hasClass('glyphicon-plus')) {
//        加载子类数据
            var html = "";
            var formData = {"path":$("#path").val(),"platform":$("#platform").val(),"name":name};
            $.ajax({
                url: prefix + "/getOneFileHistory",
                type: 'post',
                dataType: "json",
                data: formData,
                success: function(result) {
                    if(result.code == web_status.SUCCESS){
                        $.each(result.data,function (i,k) {
                            html += '<tr child-index="'+i+'" class="child-tr pid-'+id+'">';
                            html +='<td style="text-align: center; "/>';
                            html +='<td/>';
                            if(k.name==null){
                                html +='<td style="">-</td>';
                            }else {
                var showname = k.name;
                if (showname.length > 15) {
                  showname = showname.substr(0,14)+"...";
                }
                html += '<td title="'+k.name+'">'+showname+'</td>';
                            }
                            html +='<td style="">'+k.versionNum+'</td>';
                            html +='<td style="">'+k.createDate+'</td>';
                            var fileSize = k.size;
                            if (fileSize != null) {
                                var number1024 = 1<<10;
                                var number1048576 = 1<<20;
                                var number1073741824 = 1<<30;
                                if (fileSize > number1024) {
                                    if (fileSize > number1048576) {
                                        if (fileSize > number1073741824) {
                                            var fixed = (fileSize/number1073741824).toFixed(2);
                                            html +='<td style="">'+fixed+'GB</td>';
                                        }else {
                                            var fixed = (fileSize/number1048576).toFixed(2);
                                            html +='<td style="">'+fixed+'MB</td>';
                                        }
                                    }else {
                                        var fixed = (fileSize/number1024).toFixed(2);
                                        html +='<td style="">'+fixed+'KB</td>';
                                    }
                                }else {
                                    html +='<td style="">'+fileSize+'B</td>';
                                }
                            }else {
                                html +='<td style="">-</td>';
                            }
                            html +='<td style="">'+k.createUserName+'</td>';
                            html +='<td style="">'+$.table.tooltip(k.remark);+'</td>';
                            html +='<td style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; ">';
                            var fileType = k.type;
                            if (fileType != null
                                && ("jpg" == fileType.toLowerCase() || "png" == fileType.toLowerCase() || "gif" == fileType.toLowerCase() || "jpeg" == fileType.toLowerCase())) {
                                html +='<a class="btn btn-success btn-xs ' + viewImgFlag + '" href="javascript:void(0)" onclick="viewImage(\'' + k.bakPathPrefix +"/"+ k.bakName +'\')"><i class="fa fa-leaf"></i>预览</a> ';
                            }
                            if(i!=0){
                                //不是第一个的时候加还原
                                html +='<a class="btn btn-warning btn-xs " href="javascript:void(0)" onclick="rollbackFile('+k.id+')">';
                                html +='<i class="fa fa-mail-reply"/>还原</a> ';
              }else {
                                // 是第一个的时候如果是删除的加还原
                                if(k.delFlag === 1){
                                    html +='<a class="btn btn-warning btn-xs " href="javascript:void(0)" onclick="rollbackFile('+k.id+')">';
                                    html +='<i class="fa fa-mail-reply"/>还原</a> ';
                }
              }
                            if (fileType != null && ("html" == fileType.toLowerCase() || "shtml" == fileType.toLowerCase() || "css" == fileType.toLowerCase() ||
                "js" == fileType.toLowerCase()|| "txt" == fileType.toLowerCase())) {
                                html +='<a class="btn btn-info btn-xs " href="javascript:void(0)" onclick="comparedFile('+k.id+')">';
                                html +='<i class="fa fa-exchange"/>对比</a>';
                            }
                            html +='</td>';
                            html +='</tr>';
                        });
                        $(tdThis).parent().parent().after(html);
                    }else{
                        if("未登录或登录超时。请重新登录"==res.msg){
                            window.location=ctx+"login";
                        }else {
                            $.modal.alertError(result.msg);
                        }
                    }
                }
            });
//        写入之类成功之后在去掉加号等样式,防止出错
            $(tdThis).children('i').removeClass('glyphicon-plus');
            $(tdThis).children('i').removeClass('icon-plus');
            $(tdThis).children('i').addClass('glyphicon-minus');
            $(tdThis).children('i').addClass('icon-minus');
        }else {
//        减号的时候点击将子类数据移除,然后将减号变加号
            $('.pid-'+id+'').remove();
            $(tdThis).children('i').removeClass('glyphicon-minus');
            $(tdThis).children('i').removeClass('icon-minus');
            $(tdThis).children('i').addClass('glyphicon-plus');
            $(tdThis).children('i').addClass('icon-plus');
        }
    }
    //还原文件的方法
    function rollbackFile(id){
        $.modal.confirm("确认要还原该文件吗?", function() {
            var url =  prefix + "/rollbackFile";
            var data = { "id": id };
            $.operate.submit(url, "post", "json", data,function () {
                $.table.search();
            });
        });
    }
    //对比选择
    function comparedFile(id){
        $.modal.openTab('对比选择',prefix + "/selectCompared/?id="+id);
    }
  function search1() {
    var start = $("#createStartDate").val();
    var end = $("#createEndDate").val();
    var startdate = new Date(start);
    var enddate = new Date(end);
    if (startdate.getTime() > enddate.getTime()) {
      $.modal.alertError("开始时间不能大于结束时间");
      return false;
    }
    $.table.search();
  }
  function reset1() {
    $("#createUser").val("");
    $.form.reset();
  }
</script>
</body>
</html>

点击列表页之后跳转文件对比controller方法

@RequestMapping("/comparedThisFile")
public String comparedThisFile(HttpServletRequest request, ModelMap mmap)
{
  String text1path = null;
  String text2path = null;
  //String htmlPath = null;
  String platform = request.getParameter("platform");
  try {
    String leftId = request.getParameter("leftId");
    String rightId = request.getParameter("rightId");
    String profile = ConfigConstant.cosTempPath;
    PublishFileVersion left = publishFileVersionService.selectPublishFileVersionById(Long.parseLong(leftId));
//      获取左边文件路径
    String bakpath = left.getBakPathPrefix();
    String bakname = left.getBakName();
    //file.text_0
    String filename = left.getName();
    String downname = filename +"_"+left.getVersionNum();
    if (filename.contains(".")) {
      downname = filename.replace(".","_"+left.getVersionNum()+".");
    }
    if(left.getObfuscateFlag()==1){
//        混淆的时候文件名用混淆的那个名字
      bakname = left.getObfuscateSourceName();
    }
    String leftkey = bakpath + bakname;
    PublishFileVersion right = publishFileVersionService.selectPublishFileVersionById(Long.parseLong(rightId));
//      获取右边文件路径
    String bakpath2 = right.getBakPathPrefix();
    String bakname2 = right.getBakName();
    //file.text_0
    String filename2 = right.getName();
    String downname2 = filename2 +"_"+right.getVersionNum();
    if (filename2.contains(".")) {
      downname2 = filename2.replace(".","_"+right.getVersionNum()+".");
    }
    if(right.getObfuscateFlag()==1){
//        混淆的时候文件名用混淆的那个名字
      bakname2 = right.getObfuscateSourceName();
    }
    String rightkey = bakpath2 + bakname2;
    if (Constants.Platform.COS.getValue().equals(Integer.valueOf(platform))) {
      CosClientUtil cosClientUtil = new CosClientUtil();
      try {
        //下载到服务器
        text1path = profile + downname;
        cosClientUtil.download(leftkey, text1path);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      try {
        //下载到服务器
        text2path = profile + downname2;
        cosClientUtil.download(rightkey, text2path);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }else {
      text1path = bakpath+File.separator+bakname;
      text2path = bakpath2+File.separator+bakname2;
    }
    List<String> diffString = DiffHandleUtils.diffString(text1path,text2path,downname,downname2);
    //System.out.println("======diff::::::"+diffString);
    //在服务器生成一个diff.html文件,打开便可看到两个文件的对比
    //htmlPath = profile + "compareresult.html";
    String string = DiffHandleUtils.generateDiffString(diffString);
    //System.out.println("======res::::::"+string);
    mmap.put("right", string);
  }catch (Exception e) {
    e.printStackTrace();
  }finally {
    if (Constants.Platform.COS.getValue().equals(Integer.valueOf(platform))) {
      //删除下载的临时文件
      if (StringUtils.isNotEmpty(text1path)) {
        boolean delete = new File(text1path).delete();
      }
      if (StringUtils.isNotEmpty(text2path)) {
        boolean delete = new File(text2path).delete();
      }
    }
  }
  return  prefix+"/newComparedFile";
}

文件对比工具类主要方法 DiffHandleUtils.java

package com.dongao.project.utils;
import com.github.difflib.UnifiedDiffUtils;
import com.github.difflib.patch.Patch;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * @Author zhangw
 * @Date 2022/4/21 18:04
 * @Version 1.0
 */
public class DiffHandleUtils {
    /**
     * 对比两文件的差异,返回原始文件+diff格式
     *
     * @param original 原文件内容
     * @param revised  对比文件内容
     */
    public static List<String> diffString(List<String> original, List<String> revised) {
        return diffString(original, revised, null, null);
    }
    /**
     * 对比两文件的差异,返回原始文件+diff格式
     *
     * @param original         原文件内容
     * @param revised          对比文件内容
     * @param originalFileName 原始文件名
     * @param revisedFileName  对比文件名
     */
    public static List<String> diffString(List<String> original, List<String> revised, String originalFileName, String revisedFileName) {
        originalFileName = originalFileName == null ? "原始文件" : originalFileName;
        revisedFileName = revisedFileName == null ? "对比文件" : revisedFileName;
        //两文件的不同点
        Patch<String> patch = com.github.difflib.DiffUtils.diff(original, revised);
        //生成统一的差异格式
        List<String> unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(originalFileName, revisedFileName, original, patch, 0);
        if (unifiedDiff.size() == 0) {
            //如果两文件没差异则插入如下
            unifiedDiff.add("--- " + originalFileName);
            unifiedDiff.add("+++ " + revisedFileName);
            unifiedDiff.add("@@ -0,0 +0,0 @@");
        } else if (unifiedDiff.size() >= 3 && !unifiedDiff.get(2).contains("@@ -1,")) {
            //如果第一行没变化则插入@@ -0,0 +0,0 @@
            unifiedDiff.add(2, "@@ -0,0 +0,0 @@");
        }
        //原始文件中每行前加空格
        List<String> original1 = original.stream().map(v -> " " + v).collect(Collectors.toList());
        //差异格式插入到原始文件中
        return insertOrig(original1, unifiedDiff);
    }
    /**
     * 对比两文件的差异,返回原始文件+diff格式
     *
     * @param filePathOriginal 原文件路径
     * @param filePathRevised  对比文件路径
     */
    public static List<String> diffString(String filePathOriginal, String filePathRevised,String originalName,
                                          String revisedName) {
        //原始文件
        List<String> original = null;
        //对比文件
        List<String> revised = null;
        File originalFile = new File(filePathOriginal);
        File revisedFile = new File(filePathRevised);
        try {
            original = Files.readAllLines(originalFile.toPath());
            revised = Files.readAllLines(revisedFile.toPath());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return diffString(original, revised, originalName, revisedName);
    }
    /**
     * 通过两文件的差异diff生成 html文件,打开此 html文件便可看到文件对比的明细内容
     *
     * @param diffString 调用上面 diffString方法获取到的对比结果
     * @param htmlPath   生成的html路径,如:/user/var/mbos/ent/21231/diff.html
     * HTML输出接受一个Javascript对象,该对象可能有以下配置项:
     *
     * inputFormat: 输入数据的格式: 'diff' 或者 'json', 默认是'diff'
     * outputFormat: 输出数据的格式: 'line-by-line' 或者 'side-by-side', 默认是'line-by-line'
     * showFiles: 在对比之前查看文件列表,true 或者false,默认是false
     * matching: 匹配level: 'lines'用于匹配行, 'words' 用于匹配行和单词,或者设置为'none',默认为none
     * matchWordsThreshold: 单词相似度下限, 默认是0.25
     * matchingMaxComparisons: 为了匹配一组变化最多执行的比较次数,默认是2500
     * maxLineLengthHighlight: 如果行数小于此值,则仅仅执行差异突出显示,默认是10000
     * templates: 使用预备好的编译的模板替换部分html的对象。
     * rawTemplates: 具有原始未编译模板的对象替换部分html。
     * 更多参考 https://github.com/rtfpessoa/diff2html/tree/master/src/templates
     */
    public static void generateDiffHtml(List<String> diffString, String htmlPath) {
        StringBuilder builder = new StringBuilder();
        for (String line : diffString) {
            builder.append(line);
            builder.append("\n");
        }
        String template = "<!DOCTYPE html>\n" +
                "<html lang=\"en-us\">\n" +
                "  <head>\n" +
                "    <meta charset=\"utf-8\" />\n" +
                "    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css\" />\n" +
                "     <link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css\" />\n" +
                "    <script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js\"></script>\n" +
                "  </head>\n" +
                "  <script>\n" +
                "    const diffString = `\n" +
                "temp\n" +
                "`;\n" +
                "\n" +
                "\n" +
                "     document.addEventListener('DOMContentLoaded', function () {\n" +
                "      var targetElement = document.getElementById('diffElement');\n" +
                "      var configuration = {\n" +
                "        drawFileList: true,\n" +
                "        fileListToggle: false,\n" +
                "        fileListStartVisible: false,\n" +
                "        fileContentToggle: false,\n" +
                "        matching: 'words',\n" +
                "        outputFormat: 'side-by-side',\n" +
                "        synchronisedScroll: true,\n" +
                "        highlight: true,\n" +
                "        renderNothingWhenEmpty: true,\n" +
                "      };\n" +
                "      var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);\n" +
                "      diff2htmlUi.draw();\n" +
                "      diff2htmlUi.highlightCode();\n" +
                "    });\n" +
                "  </script>\n" +
                "  <body>\n" +
                "    <div id=\"diffElement\"></div>\n" +
                "  </body>\n" +
                "</html>";
        String string = builder.toString();
        string = string.replace("/", "\\/");
        template = template.replace("temp", string);
        FileWriter f = null; //文件读取为字符流
        try {
            f = new FileWriter(htmlPath);
            BufferedWriter buf = new BufferedWriter(f); //文件加入缓冲区
            buf.write(template); //向缓冲区写入
            buf.close(); //关闭缓冲区并将信息写入文件
            f.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 通过两文件的差异diff生成 string,打开此 html文件便可看到文件对比的明细内容
     * @param diffString
     * @return
     */
    public static String generateDiffString(List<String> diffString) {
        StringBuilder builder = new StringBuilder();
        builder.append("`");
        for (String line : diffString) {
            //对比页面开始符号冲突
            if (line.contains("`")) {
                line = line.replace("`","\\`");
            }
            //</script>结束标志</冲突
            if (line.contains("</")) {
                line = line.replace("</", "<\\/");
            }
            //正则\冲突
            if (line.contains("\\") && line.contains("^")) {
                line = line.replace("\\", "\\\\");
            }
            //页面取值冲突 ${}
            if (line.contains("${")) {
                line = line.replace("${", "$\\{");
            }
            builder.append(line);
            builder.append("\n");
        }
        builder.append("`");
        String string = builder.toString();
        return string;
    }
    //统一差异格式插入到原始文件
    public static List<String> insertOrig(List<String> original, List<String> unifiedDiff) {
        List<String> result = new ArrayList<>();
        //unifiedDiff中根据@@分割成不同行,然后加入到diffList中
        List<List<String>> diffList = new ArrayList<>();
        List<String> d = new ArrayList<>();
        for (int i = 0; i < unifiedDiff.size(); i++) {
            String u = unifiedDiff.get(i);
            if (u.startsWith("@@") && !"@@ -0,0 +0,0 @@".equals(u) && !u.contains("@@ -1,")) {
                List<String> twoList = new ArrayList<>();
                twoList.addAll(d);
                diffList.add(twoList);
                d.clear();
                d.add(u);
                continue;
            }
            if (i == unifiedDiff.size() - 1) {
                d.add(u);
                List<String> twoList = new ArrayList<>();
                twoList.addAll(d);
                diffList.add(twoList);
                d.clear();
                break;
            }
            d.add(u);
        }
        //将diffList和原始文件original插入到result,返回result
        for (int i = 0; i < diffList.size(); i++) {
            List<String> diff = diffList.get(i);
            List<String> nexDiff = i == diffList.size() - 1 ? null : diffList.get(i + 1);
            //含有@@的一行
            String simb = i == 0 ? diff.get(2) : diff.get(0);
            String nexSimb = nexDiff == null ? null : nexDiff.get(0);
            //插入到result
            insert(result, diff);
            //解析含有@@的行,得到原文件从第几行开始改变,改变了多少(即增加和减少的行)
            Map<String, Integer> map = getRowMap(simb);
            if (null != nexSimb) {
                Map<String, Integer> nexMap = getRowMap(nexSimb);
                int start = 0;
                if (map.get("orgRow") != 0) {
                    start = map.get("orgRow") + map.get("orgDel") - 1;
                }
                int end = nexMap.get("revRow") - 2;
                //插入不变的
                insert(result, getOrigList(original, start, end));
            }
            if (simb.contains("@@ -1,") && null == nexSimb) {
                insert(result, getOrigList(original, 0, original.size() - 1));
            } else if (null == nexSimb && map.get("orgRow") < original.size()) {
                insert(result, getOrigList(original, map.get("orgRow"), original.size() - 1));
            }
        }
        return result;
    }
    //将源文件中没变的内容插入result
    public static void insert(List<String> result, List<String> noChangeContent) {
        for (String ins : noChangeContent) {
            result.add(ins);
        }
    }
    //解析含有@@的行得到修改的行号删除或新增了几行
    public static Map<String, Integer> getRowMap(String str) {
        Map<String, Integer> map = new HashMap<>();
        if (str.startsWith("@@")) {
            String[] sp = str.split(" ");
            String org = sp[1];
            String[] orgSp = org.split(",");
            //源文件要删除行的行号
            map.put("orgRow", Integer.valueOf(orgSp[0].substring(1)));
            //源文件删除的行数
            map.put("orgDel", Integer.valueOf(orgSp[1]));
            String[] revSp = org.split(",");
            //对比文件要增加行的行号
            map.put("revRow", Integer.valueOf(revSp[0].substring(1)));
            map.put("revAdd", Integer.valueOf(revSp[1]));
        }
        return map;
    }
    //从原文件中获取指定的部分行
    public static List<String> getOrigList(List<String> original1, int start, int end) {
        List<String> list = new ArrayList<>();
        if (start <= end && end < original1.size()) {
            for (; start <= end; start++) {
                list.add(original1.get(start));
            }
        }
        return list;
    }
}

返回页面展示结果如开始,newComparedFile.html 页面代码

<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" th:href="@{/css/github.min.css}"/>
    <link rel="stylesheet" type="text/css" th:href="@{/css/diff2html.min.css}"/>
    <script type="text/javascript" th:src="@{/js/diff2html-ui.min.js}"></script>
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
</head>
<script>
    const diffString = [(${right})];
    document.addEventListener('DOMContentLoaded', function () {
        var targetElement = document.getElementById('diffElement');
        var configuration = {
            //在diff之前显示文件列表:true或false,默认值为true
            drawFileList: true,
            //允许切换文件摘要列表:true或false,默认值为true
            fileListToggle: false,
            //选择文件摘要列表是否开始可见:true或false,默认值为false
            fileListStartVisible: false,
            //允许切换每个文件内容:true或false,默认值为true
            fileContentToggle: false,
            //匹配level: 'lines'用于匹配行, 'words' 用于匹配行和单词,或者设置为'none',默认为none
            matching: 'words',
            //输出数据的格式: 'line-by-line' 或者 'side-by-side', 默认是'line-by-line'
            outputFormat: 'side-by-side',
            synchronisedScroll: true,
            highlight: true,
            renderNothingWhenEmpty: true,
        };
        var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
        diff2htmlUi.draw();
        diff2htmlUi.highlightCode();
        var texts = $(".d2h-code-wrapper").find("td[class='d2h-info']").find("div");
        if (texts.length) {
            $.each(texts,function (index,value) {
                var _this = $(this);
                //console.log(_this.text())
                _this.text("");
            });
        }
    });
</script>
<body>
<div id="diffElement"></div>
</body>
</html>

参考文献:https://github.com/rtfpessoa/diff2html#diff2htmlui-api

https://diff2html.xyz/

本文主要用于记录日常开发,仅供学习参考

相关文章
|
1月前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
74 9
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2月前
|
Java
Java“解析时到达文件末尾”解决
在Java编程中,“解析时到达文件末尾”通常指在读取或处理文件时提前遇到了文件结尾,导致程序无法继续读取所需数据。解决方法包括:确保文件路径正确,检查文件是否完整,使用正确的文件读取模式(如文本或二进制),以及确保读取位置正确。合理设置缓冲区大小和循环条件也能避免此类问题。
490 2
|
2月前
|
Java
利用GraalVM将java文件变成exe可执行文件
这篇文章简明地介绍了如何使用GraalVM将一个简单的Java程序编译成exe可执行文件,首先通过javac命令编译Java文件生成class文件,然后使用native-image命令将class文件转换成独立的exe文件,并展示了如何运行这个exe文件。
101 0
利用GraalVM将java文件变成exe可执行文件
|
16天前
|
Java
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
79 34
|
1月前
|
消息中间件 存储 Java
RocketMQ文件刷盘机制深度解析与Java模拟实现
【11月更文挑战第22天】在现代分布式系统中,消息队列(Message Queue, MQ)作为一种重要的中间件,扮演着连接不同服务、实现异步通信和消息解耦的关键角色。Apache RocketMQ作为一款高性能的分布式消息中间件,广泛应用于实时数据流处理、日志流处理等场景。为了保证消息的可靠性,RocketMQ引入了一种称为“刷盘”的机制,将消息从内存写入到磁盘中,确保消息持久化。本文将从底层原理、业务场景、概念、功能点等方面深入解析RocketMQ的文件刷盘机制,并使用Java模拟实现类似的功能。
41 3
|
1月前
|
Java 测试技术 Maven
Maven clean 提示文件 java.io.IOException
在使用Maven进行项目打包时,遇到了`Failed to delete`错误,尝试手动删除目标文件也失败,提示`java.io.IOException`。经过分析,发现问题是由于`sys-info.log`文件被其他进程占用。解决方法是关闭IDEA和相关Java进程,清理隐藏的Java进程后重新尝试Maven clean操作。最终问题得以解决。总结:遇到此类问题时,可以通过任务管理器清理相关进程或重启电脑来解决。
|
1月前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
87 2
|
1月前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
57 4
|
1月前
|
存储 Java API
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
62 4