• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2022 Shenzhen Kaihong Digital Industry Development Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 package com.sk.dialog;
16 
17 import com.intellij.notification.NotificationType;
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.ui.ValidationInfo;
21 import com.sk.action.BrowseAction;
22 import com.sk.action.GenAction;
23 import com.sk.action.ScriptAction;
24 import com.sk.utils.FileInfo;
25 import com.sk.utils.FileUtil;
26 import com.sk.utils.GenNotification;
27 import org.apache.http.util.TextUtils;
28 import org.jetbrains.annotations.Nullable;
29 
30 
31 import javax.swing.JDialog;
32 import javax.swing.JPanel;
33 import javax.swing.JTextField;
34 import javax.swing.JRadioButton;
35 import javax.swing.JButton;
36 import javax.swing.JComboBox;
37 import javax.swing.JComponent;
38 import javax.swing.KeyStroke;
39 import java.awt.event.KeyEvent;
40 import java.awt.event.WindowAdapter;
41 import java.awt.event.WindowEvent;
42 import java.awt.event.WindowListener;
43 import java.io.BufferedReader;
44 import java.io.File;
45 import java.io.FileInputStream;
46 import java.io.FileNotFoundException;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.io.InputStreamReader;
51 import java.nio.charset.StandardCharsets;
52 import java.nio.file.Files;
53 import java.nio.file.Path;
54 import java.nio.file.Paths;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.List;
58 import java.util.regex.Matcher;
59 import java.util.regex.Pattern;
60 
61 /**
62  * 配置对话框
63  *
64  * @author: xudong
65  * @see: generator dialog
66  * @version: v1.0.0
67  * @since 2022-02-21
68  */
69 public class GenerateDialogPane extends JDialog {
70     private static final Logger LOG = Logger.getInstance(GenerateDialogPane.class);
71     private static final String FILE_NAME_REGEX = "(\\@ohos\\.)(.*?)(\\.d\\.ts)";
72     private static final Pattern FILE_NAME_PATTERN = Pattern.compile(FILE_NAME_REGEX, Pattern.CASE_INSENSITIVE);
73     private static final String NAMESPACE_REGEX = "declare namespace ([a-zA-Z_0-9]+) *(\\{)";
74     private static final Pattern NAMESPACE_PATTERN = Pattern.compile(NAMESPACE_REGEX, Pattern.CASE_INSENSITIVE);
75     private static final String CMAKE_SETCXX_TEMPLATE = "cmake_minimum_required(VERSION 3.4.1)"
76             + FileUtil.getNewline() + "project(napi_lib)" + FileUtil.getNewline() + "set(CMAKE_CXX_STANDARD 17)"
77             + FileUtil.getNewline() + FileUtil.getNewline();
78     private static final String CMAKE_ADD_LIB_TEMPLATE =
79             "add_library(LIBNAME SHARED PATH/tool_utility.cpp PATH/FILE_PREFIX.cpp PATH/FILE_PREFIX_middle.cpp)";
80     private static final String CMAKE_LINK_TEMPLATE =
81             "target_link_libraries(LIBNAME PUBLIC libace_napi.z.so libuv.so)";
82 
83     private final Project project;
84     private List<String> tsFileList = new ArrayList<>();
85     private JPanel contentPane;
86 
87     private JTextField textFieldInterPath;
88     private JTextField textFieldGenPath;
89     private JTextField textFieldScriptPath;
90     private JRadioButton radioButton;
91     private JButton buttonSelectInter;
92     private JButton buttonSelectGenPath;
93     private JButton buttonSelectScriptPath;
94     private JComboBox comboBox;
95     private boolean generateSuccess = true;
96     private String sErrorMessage = "";
97     private String interFileOrDir;
98     private String genOutDir;
99     private String scriptOutDir;
100     private String numberType;
101 
102 
103     /**
104      * 构造函数
105      *
106      * @param project       projectid
107      * @param interFilePath 接口文件路径
108      * @param genDir        生成框架文件路径
109      * @param scriptDir     脚本目录
110      */
GenerateDialogPane(Project project, String interFilePath, String genDir, String scriptDir)111     public GenerateDialogPane(Project project, String interFilePath, String genDir, String scriptDir) {
112         setContentPane(contentPane);
113         setModal(true);
114         this.project = project;
115         this.interFileOrDir = interFilePath;
116         this.genOutDir = genDir;
117         this.scriptOutDir = scriptDir;
118 
119         textFieldInterPath.setText(interFileOrDir);
120         textFieldGenPath.setText(genOutDir);
121         textFieldScriptPath.setText(genOutDir);
122 
123         // call onCancel() on ESCAPE
124         contentPane.registerKeyboardAction(actionEvent -> onCancel(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
125                 JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
126 
127         BrowseAction browseAction = new BrowseAction(project, buttonSelectInter, textFieldInterPath,
128                 textFieldGenPath, textFieldScriptPath);
129         buttonSelectInter.addActionListener(browseAction);
130         buttonSelectGenPath.addActionListener(new GenAction(buttonSelectGenPath, textFieldGenPath));
131         buttonSelectScriptPath.addActionListener(new ScriptAction(buttonSelectScriptPath, textFieldScriptPath));
132 
133     }
134 
135     @Override
addWindowListener(WindowListener windowListener)136     public synchronized void addWindowListener(WindowListener windowListener) {
137         super.addWindowListener(windowListener);
138         new WindowAdapter() {
139             /**
140              * close dialog
141              *
142              * @param windowEvent WindowEvent
143              */
144             @Override
145             public void windowClosing(WindowEvent windowEvent) {
146                 onCancel();
147             }
148         };
149     }
150 
151     /**
152      * 验证文本选择框是否空。是否替换已存在的内容
153      *
154      * @return ValidationInfo 返回不符要求的信息。
155      */
156     @Nullable
validationInfo()157     public ValidationInfo validationInfo() {
158         ValidationInfo validationInfo = null;
159         String fileInter = textFieldInterPath.getText();
160         String scriptDir = textFieldScriptPath.getText();
161         String filegypDir = textFieldGenPath.getText();
162         boolean isEmptyFile =
163                 TextUtils.isEmpty(fileInter) || TextUtils.isEmpty(scriptDir) || TextUtils.isEmpty(filegypDir);
164         if (isEmptyFile) {
165             String warnMsg = "接口文件、框架、编译脚本路径不能为空";
166             warningMessage(warnMsg);
167             validationInfo = new ValidationInfo(warnMsg);
168             return validationInfo;
169         }
170         File file = new File(filegypDir + "/binding.gyp");
171         if (file.exists()) {
172             ConfirmDialog confirmDialog = new ConfirmDialog("是否替换已存在的编译脚本?");
173             if (!confirmDialog.showAndGet()) {
174                 validationInfo = new ValidationInfo(String.format("不替换现有编译脚本:%s", file));
175                 return validationInfo;
176             }
177         }
178         return validationInfo;
179     }
180 
onCancel()181     private void onCancel() {
182         dispose();
183     }
184 
warningMessage(String title)185     private void warningMessage(String title) {
186         String notifyContent = "请选择接口文件或文件夹,生成框架路径,编译脚本路径";
187         GenNotification.notifyMessage(this.project, notifyContent, title, NotificationType.WARNING);
188     }
189 
190     /**
191      * 执行主程序入口
192      *
193      * @return 执行状态
194      */
runFun()195     public boolean runFun() {
196         GenNotification.notifyMessage(this.project, "", "Generating Napi", NotificationType.INFORMATION);
197         interFileOrDir = textFieldInterPath.getText();
198         genOutDir = textFieldGenPath.getText();
199         scriptOutDir = textFieldScriptPath.getText();
200         numberType = comboBox.getSelectedItem().toString();
201         String command;
202         command = genCommand();
203 
204         File outPath = new File(textFieldGenPath.getText());
205         List<FileInfo> oldFileList = getFileInfoList(outPath);
206         try {
207             if (!TextUtils.isEmpty(command) && callExtProcess(command)) {
208                 List<FileInfo> newFileList = getFileInfoList(outPath);
209                 newFileList.removeAll(oldFileList);
210 
211                 GenNotification.notifyGenResult(project, newFileList, "Generate Napi Successfully",
212                         NotificationType.INFORMATION);
213                 return true;
214             }
215         } catch (IOException | InterruptedException ex) {
216             GenNotification.notifyMessage(project, textFieldGenPath.getText(), "Command exec error",
217                     NotificationType.ERROR);
218             LOG.error(ex);
219         }
220         return false;
221     }
222 
223     /**
224      * 生成命令行指令
225      *
226      * @return 返回命令行执行内容
227      */
genCommand()228     private String genCommand() {
229         String sysName = System.getProperties().getProperty("os.name").toUpperCase();
230         String tmpDirFile = System.getProperty("java.io.tmpdir");
231         if (sysName.contains("WIN")) {
232             copyFileToLocalPath("napi_generator-win");
233             tmpDirFile += "napi_generator-win.exe";
234         } else if (sysName.contains("LINUX")) {
235             copyFileToLocalPath("napi_generator-linux");
236             tmpDirFile += "napi_generator-linux";
237         } else {
238             copyFileToLocalPath("napi_generator-macos");
239             tmpDirFile += "napi_generator-macos";
240         }
241         File file = new File(tmpDirFile);
242         String command = file.toString();
243         String inArgs = genInArgs(interFileOrDir);
244         command += inArgs + " -o " + genOutDir + " -i " + radioButton.isSelected() + " -n " + genNumbertypeArgs();
245         return command;
246     }
247 
248     /**
249      * 生成 -n 输入参数。
250      *
251      * @return 生成后的值-n的值
252      */
genNumbertypeArgs()253     private String genNumbertypeArgs() {
254         String type = "uint32_t";
255         if (numberType != "") {
256             type = numberType;
257         }
258         return type;
259     }
260 
261     /**
262      * 拷贝文件到本地临时目录
263      *
264      * @param fileName 文件名
265      */
copyFileToLocalPath(String fileName)266     private void copyFileToLocalPath(String fileName) {
267         String sysName = System.getProperties().getProperty("os.name").toUpperCase();
268         String tmpDirFile = System.getProperty("java.io.tmpdir");
269         String execFn;
270         if (sysName.contains("WIN")) {
271             execFn = "cmds/win/" + fileName + ".exe";
272             tmpDirFile += fileName + ".exe";
273         } else if (sysName.contains("LINUX")) {
274             execFn = "cmds/linux/" + fileName;
275             tmpDirFile += fileName;
276         } else {
277             execFn = "cmds/mac/" + fileName;
278             tmpDirFile += fileName;
279         }
280         try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(execFn)) {
281             if (inputStream == null) {
282                 throw new IOException("exec File InputStream is Null");
283             }
284             byte[] bs = inputStream.readAllBytes();
285             writeTmpFile(tmpDirFile, bs);
286             if (sysName.contains("LINUX") || sysName.contains("MAC OS")) {
287                 executable(tmpDirFile);
288             }
289         } catch (IOException | InterruptedException e) {
290             GenNotification.notifyMessage(this.project, e.getMessage(), "Can not Find File:" + execFn,
291                     NotificationType.ERROR);
292             LOG.error(e);
293         }
294     }
295 
296     /**
297      * 生成 -f -d 输入参数。
298      *
299      * @param fileOrDir 选中的文件或文件夹路径
300      * @return 生成后的 -f -d的值
301      */
genInArgs(String fileOrDir)302     private String genInArgs(String fileOrDir) {
303         tsFileList.clear();
304         String[] interArr = fileOrDir.split(",");
305         StringBuilder tsParam = new StringBuilder(" -f ");
306         StringBuilder dirParam = new StringBuilder(" -d ");
307         String inputCommand = "";
308         if (interArr.length > 0) {
309             for (String interStr : interArr) {
310                 File interFile = new File(interStr);
311                 if (interFile.isDirectory()) {
312                     dirParam.append(interStr).append(" ");
313                     for (File tsFile : interFile.listFiles()) {
314                         tsFileList.add(tsFile.getPath());
315                     }
316                 } else {
317                     tsParam.append(interStr).append(",");
318                     tsFileList.add(interStr);
319                 }
320             }
321             if (!TextUtils.isBlank(tsParam.toString().replaceAll("-f", ""))) {
322                 inputCommand += tsParam.substring(0, tsParam.length() - 1);
323             }
324             if (!TextUtils.isBlank(dirParam.toString().replace("-d", ""))) {
325                 inputCommand += dirParam.substring(0, dirParam.length() - 1);
326             }
327         }
328         return inputCommand;
329     }
330 
callExtProcess(String command)331     private boolean callExtProcess(String command) throws IOException, InterruptedException {
332 
333         if (TextUtils.isEmpty(command)) {
334             GenNotification.notifyMessage(this.project, "执行命令文件为空", "空命令行提示", NotificationType.ERROR);
335             return false;
336         }
337         Process process = Runtime.getRuntime().exec(command);
338         genResultLog(process);
339         StreamConsumer errConsumer = new StreamConsumer(process.getErrorStream());
340         StreamConsumer outputConsumer = new StreamConsumer(process.getInputStream());
341         errConsumer.start();
342         outputConsumer.start();
343 
344 
345         if (generateSuccess) {
346             writeCompileCfg();
347         } else {
348             GenNotification.notifyMessage(project, sErrorMessage, "提示", NotificationType.ERROR);
349             return false;
350         }
351 
352         errConsumer.join();
353         outputConsumer.join();
354         return true;
355     }
356 
357     /**
358      * 获得 pathA 相对于 pathB的相对路径
359      *
360      * @param pathA 路径A,如 D:\xx\yy\zz\a1\a2
361      * @param pathB 路径B, 如 D:\xx\yy\zz\b1\b2\b3
362      * @return pathA 相对于 pathB的相对路径: ../../../a1/a2/
363      */
getRelativePath(String pathA, String pathB)364     private String getRelativePath(String pathA, String pathB) {
365         String separatorStr = File.separator.equals("\\") ? "\\\\" : File.separator;
366         String[] pathAList = pathA.split(separatorStr);
367         String[] pathBList = pathB.split(separatorStr);
368 
369         int pos = 0;
370         for (; pos < pathAList.length && pos < pathBList.length; ++pos) {
371             if (!pathAList[pos].equals(pathBList[pos])) {
372                 // 找到两个path路径存在差异的位置
373                 break;
374             }
375         }
376         // 截取pathA和pathB路径字符串的差异部分
377         String[] diffPathAList = Arrays.copyOfRange(pathAList, pos, pathAList.length);
378         String[] diffPathBList = Arrays.copyOfRange(pathBList, pos, pathBList.length);
379 
380         // pathA的差异字符串作为相对路径的结尾部分
381         String pathAStr = String.join("/", diffPathAList);
382         pathAStr = pathAStr.isBlank() ? "" : pathAStr + "/";
383 
384         // 根据pathB的差异目录层级生成向上跳转字符串
385         String rollbackPath = "";
386         for (int i = 0; i < diffPathBList.length; ++i) {
387             rollbackPath += "../";
388         }
389         rollbackPath = rollbackPath.isEmpty() ? "./" : rollbackPath;
390 
391         // 相对路径 = 向上跳转部分 + pathA的差异部分
392         return rollbackPath + pathAStr;
393     }
394 
395     /**
396      * 获取NAPI工具生成的cpp文件前缀
397      *
398      * @param tsFilePath ts接口文件名
399      * @return cpp文件前缀
400      */
getCppNamePrefix(String tsFilePath)401     private String getCppNamePrefix(String tsFilePath) {
402         File tsFile = new File(tsFilePath);
403 
404         // NAPI工具中cpp前缀名取的是ts文件中声明的首个namespace的名称,插件这里按同样方法获取。
405         try (InputStreamReader read = new InputStreamReader(new FileInputStream(tsFile), StandardCharsets.UTF_8);
406             BufferedReader bufferedReader = new BufferedReader(read)) {
407             String line = "";
408             while ((line = bufferedReader.readLine()) != null) {
409                 // 找到 "declare namespace" 这一行并将 namespace名称作为cpp文件前缀名返回。
410                 Matcher tsNamespaceMatcher = NAMESPACE_PATTERN.matcher(line);
411                 if (tsNamespaceMatcher.find()) {
412                     return tsNamespaceMatcher.group(1);
413                 }
414             }
415         } catch (FileNotFoundException foundException) {
416             LOG.error("The ts file " + tsFilePath + " does not exist.");
417         } catch (IOException ioException) {
418             LOG.error("Failed to read file, error: " + ioException);
419         }
420         return "";
421     }
422 
423     /**
424      * 使用 ts文件@ohos.xxx.d.ts中的xxx作为编译c++lib库的名字
425      *
426      * @param tsFileName ts文件名
427      * @return 解析出的lib库名称
428      */
getLibNameFromTsFile(String tsFileName)429     private String getLibNameFromTsFile(String tsFileName) {
430         Matcher tsFileNameMatcher = FILE_NAME_PATTERN.matcher(tsFileName);
431         if (!tsFileNameMatcher.find()) {
432             LOG.warn("Invalid ts file name format, should be @ohos.xxx.d.ts.");
433             return tsFileName;
434         }
435         return tsFileNameMatcher.group(2);
436     }
437 
438     /**
439      * 生成编译文件
440      */
writeCompileCfg()441     private void writeCompileCfg() {
442         FileUtil fileUtil = new FileUtil();
443         String cmakeFilePath = fileUtil.makeFile(scriptOutDir + "/CMakeLists.txt");
444         if (TextUtils.isEmpty(cmakeFilePath)) {
445             LOG.info("makeFile is fail");
446             return;
447         }
448 
449         try {
450             // 获取cpp文件相对于CMakeList.txt文件的路径
451             String cppRelativePath = getRelativePath(new File(genOutDir).getPath(), new File(scriptOutDir).getPath());
452 
453             // 生成 CMakeList.txt文件内容
454             StringBuilder cmakeBuilder = new StringBuilder(CMAKE_SETCXX_TEMPLATE);
455             for (String tsFilePath : tsFileList) {
456                 String cppNamePrefix = getCppNamePrefix(tsFilePath);
457                 String libName = getLibNameFromTsFile(new File(tsFilePath).getName());
458                 String libStr = CMAKE_ADD_LIB_TEMPLATE.replaceAll("LIBNAME", libName)
459                         .replaceAll("PATH/", cppRelativePath).replaceAll("FILE_PREFIX", cppNamePrefix);
460                 cmakeBuilder.append(libStr).append(FileUtil.getNewline());
461 
462                 cmakeBuilder.append(CMAKE_LINK_TEMPLATE.replaceAll("LIBNAME", libName))
463                         .append(FileUtil.getNewline());
464             }
465             fileUtil.writeContentToFile(cmakeFilePath, cmakeBuilder.toString());
466 
467             // 需要在main文件夹下创建cpp目录, 如果没有此目录,DevEco 3.0版本编译时不会编译任何目录中的c++代码。
468             Path path = Paths.get(project.getBasePath() + "/entry/src/main/cpp");
469             Files.createDirectories(path);
470 
471             // 在{ProjectRoot}/entry/build-profile.json5 中增加 externalNativeOptions 配置
472             String buildJsonFilePath = project.getBasePath() + "/entry/build-profile.json5";
473 
474             // 获取CMakeLists.txt相对于build-profile.json5构建文件的相对路径
475             String cmakeRelativePath = getRelativePath(new File(cmakeFilePath).getParent(),
476                     new File(buildJsonFilePath).getParent());
477 
478             fileUtil.writeBuildJsonFile(buildJsonFilePath, cmakeRelativePath + "CMakeLists.txt");
479         } catch (IOException ioException) {
480             LOG.error("writeCommand io error" + ioException);
481         }
482     }
483 
484     /**
485      * 赋值可执行文件权限。
486      *
487      * @param execFn 可执行命令
488      * @throws IOException          打开文件异常
489      * @throws InterruptedException 中断异常
490      */
executable(String execFn)491     private void executable(String execFn) throws IOException, InterruptedException {
492         callExtProcess("chmod a+x " + execFn);
493     }
494 
495     /**
496      * 拷贝可执行文件到临时文件夹
497      *
498      * @param path 目标文件路径
499      * @param bs   字节内容
500      * @throws IOException exception
501      */
writeTmpFile(String path, byte[] bs)502     private void writeTmpFile(String path, byte[] bs) throws IOException {
503         File file = new File(path);
504         if (!file.exists()) {
505             boolean isNewFile = file.createNewFile();
506             if (!isNewFile) {
507                 LOG.info("writeTmpFile createNewFile error");
508             }
509         }
510         FileOutputStream fw = new FileOutputStream(file);
511         fw.write(bs, 0, bs.length);
512         fw.close();
513     }
514 
515     /**
516      * 获取生成成功结果文件。
517      *
518      * @param process 进程ID
519      */
genResultLog(Process process)520     private void genResultLog(Process process) {
521         BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
522         BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()));
523         String sErr;
524         String sOut;
525         sErr = getErrorResult(stdError);
526         if (TextUtils.isEmpty(sErr)) {
527             sOut = genInputLog(stdInput);
528             if (!generateIsSuccess(sOut)) {
529                 sErrorMessage = sOut;
530             }
531             return;
532         }
533         generateSuccess = false;
534         sErrorMessage = sErr;
535     }
536 
537     /**
538      * 获取生成失败结果文件。
539      *
540      * @param stdError error buff
541      * @return ErrorResult
542      */
getErrorResult(BufferedReader stdError)543     private String getErrorResult(BufferedReader stdError) {
544         StringBuilder sErr = new StringBuilder();
545         while (true) {
546             String sTmp;
547             try {
548                 if ((sTmp = stdError.readLine()) == null) {
549                     break;
550                 }
551                 sErr.append(sTmp).append(FileUtil.getNewline());
552             } catch (IOException ioException) {
553                 LOG.error(" genResultLog stdInput error" + ioException);
554             }
555         }
556         return sErr.toString();
557     }
558 
generateIsSuccess(String sOut)559     private boolean generateIsSuccess(String sOut) {
560         generateSuccess = sOut.contains("success") || TextUtils.isEmpty(sOut);
561         return generateSuccess;
562     }
563 
564     /**
565      * 获取生成文本内容。
566      *
567      * @param stdInput input buff
568      * @return 返回当前输入框内容
569      */
genInputLog(BufferedReader stdInput)570     private String genInputLog(BufferedReader stdInput) {
571         StringBuilder sOut = new StringBuilder();
572         while (true) {
573             String sTmp;
574             try {
575                 if ((sTmp = stdInput.readLine()) == null) {
576                     break;
577                 }
578                 sOut.append(sTmp).append(FileUtil.getNewline());
579             } catch (IOException ioException) {
580                 LOG.error(" genResultLog stdInput error" + ioException);
581             }
582         }
583         return sOut.toString();
584     }
585 
586     static class StreamConsumer extends Thread {
587         InputStream is;
588 
StreamConsumer(InputStream is)589         StreamConsumer(InputStream is) {
590             super.setName("StreamConsumer");
591             this.is = is;
592         }
593 
594         @Override
run()595         public void run() {
596             try {
597                 InputStreamReader isr = new InputStreamReader(is);
598                 BufferedReader br = new BufferedReader(isr);
599                 String line;
600                 while ((line = br.readLine()) != null) {
601                     LOG.error("StreamConsumer" + line);
602                 }
603             } catch (IOException ioException) {
604                 LOG.error("StreamConsumer io error" + ioException);
605             }
606         }
607     }
608 
609     /**
610      * 获取指定输出目录下的文件列表
611      *
612      * @param outPath 输出目录
613      * @return 文件信息列表
614      */
getFileInfoList(File outPath)615     public List<FileInfo> getFileInfoList(File outPath) {
616         List<FileInfo> fileInfoList = new ArrayList<>();
617         File[] files = outPath.listFiles();
618         for (File file : files) {
619             fileInfoList.add(new FileInfo(file));
620         }
621         return fileInfoList;
622     }
623 
624 
getContentPanel()625     JPanel getContentPanel() {
626         return contentPane;
627     }
628 
629 }
630