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