From d7596968cd5c7615e94950be5a913eb0c1e96359 Mon Sep 17 00:00:00 2001 From: Aharon Abadi <34122871+abadiwhitesource@users.noreply.github.com> Date: Thu, 28 Nov 2019 16:18:55 +0200 Subject: [PATCH] first commit --- .gitignore | 10 + .whitesource | 8 + LICENSE.txt | 202 + README.EUA.md | 9 + README.md | 40 + .../whitesource/update-request.txt | 1 + pom.xml | 537 + shiftleft.json | 5 + .../whitesource/agent/ConfigPropertyKeys.java | 252 + .../java/org/whitesource/agent/Constants.java | 124 + .../agent/DependencyCalculator.java | 79 + .../agent/DependencyInfoFactory.java | 424 + .../whitesource/agent/FileSystemScanner.java | 526 + .../agent/ProjectConfiguration.java | 69 + .../org/whitesource/agent/ProjectsSender.java | 479 + .../whitesource/agent/SingleFileScanner.java | 39 + .../org/whitesource/agent/TempFolders.java | 73 + .../org/whitesource/agent/ViaComponents.java | 37 + .../org/whitesource/agent/ViaLanguage.java | 19 + .../agent/archive/ArchiveExtractor.java | 600 + .../resolver/AbstractDependencyResolver.java | 113 + .../agent/dependency/resolver/BomFile.java | 172 + .../agent/dependency/resolver/BomParser.java | 64 + .../CocoaPodsDependencyCollector.java | 159 + .../CocoaPodsDependencyResolver.java | 142 + .../resolver/DependencyCollector.java | 44 + .../resolver/DependencyResolutionService.java | 389 + .../agent/dependency/resolver/IBomParser.java | 23 + .../dependency/resolver/ResolutionResult.java | 79 + .../dependency/resolver/ResolvedFolder.java | 47 + .../resolver/ViaMultiModuleAnalyzer.java | 121 + .../resolver/bower/BowerBomParser.java | 60 + .../bower/BowerDependencyResolver.java | 122 + .../bower/BowerLsJsonDependencyCollector.java | 138 + .../resolver/docker/AbstractParser.java | 60 + .../resolver/docker/AlpineParser.java | 100 + .../resolver/docker/ArchLinuxParser.java | 137 + .../resolver/docker/DebianParser.java | 121 + .../resolver/docker/DockerImage.java | 79 + .../resolver/docker/DockerResolver.java | 527 + .../dependency/resolver/docker/Package.java | 71 + .../dependency/resolver/docker/RpmParser.java | 98 + .../remotedocker/AbstractRemoteDocker.java | 389 + .../AbstractRemoteDockerImage.java | 83 + .../remotedocker/RemoteDockersManager.java | 56 + .../amazon/DockerImageAmazon.java | 59 + .../amazon/RemoteDockerAmazonECR.java | 366 + .../docker/remotedocker/azure/AzureCli.java | 64 + .../remotedocker/azure/AzureDockerImage.java | 33 + .../remotedocker/azure/AzureRemoteDocker.java | 249 + .../dotNet/DotNetDependencyResolver.java | 46 + .../dotNet/DotNetRestoreCollector.java | 28 + .../resolver/dotNet/RestoreCollector.java | 136 + .../resolver/go/GoDependencyManager.java | 30 + .../resolver/go/GoDependencyResolver.java | 1034 ++ .../dependency/resolver/gradle/GradleCli.java | 81 + .../gradle/GradleDependencyResolver.java | 405 + .../resolver/gradle/GradleLinesParser.java | 499 + .../resolver/gradle/GradleMvnCommand.java | 24 + .../resolver/hex/HexDependencyResolver.java | 447 + .../resolver/html/HtmlDependencyResolver.java | 223 + .../maven/MavenDependencyResolver.java | 200 + .../resolver/maven/MavenLinesParser.java | 94 + .../resolver/maven/MavenPomParser.java | 162 + .../maven/MavenTreeDependencyCollector.java | 324 + .../dependency/resolver/npm/NpmBomParser.java | 130 + .../resolver/npm/NpmDependencyResolver.java | 479 + .../npm/NpmLsJsonDependencyCollector.java | 320 + .../dependency/resolver/npm/RegistryType.java | 11 + .../resolver/npm/YarnDependencyCollector.java | 196 + .../nuget/NugetDependencyResolver.java | 158 + .../resolver/nuget/NugetRestoreCollector.java | 30 + .../packagesConfig/NugetConfigFileType.java | 9 + .../packagesConfig/NugetCsprojItemGroup.java | 49 + .../packagesConfig/NugetCsprojPackages.java | 28 + .../nuget/packagesConfig/NugetPackage.java | 61 + .../packagesConfig/NugetPackageInterface.java | 15 + .../nuget/packagesConfig/NugetPackages.java | 44 + .../NugetPackagesConfigXmlParser.java | 148 + .../packagesConfig/PackageReference.java | 46 + .../nuget/packagesConfig/ReferenceTag.java | 60 + .../packageManger/LinuxPkgManagerCommand.java | 19 + .../PackageManagerExtractor.java | 215 + .../resolver/php/PhpDependencyResolver.java | 298 + .../resolver/php/phpModel/PackageSource.java | 58 + .../resolver/php/phpModel/PhpModel.java | 70 + .../resolver/php/phpModel/PhpPackage.java | 94 + .../resolver/python/DependenciesFileType.java | 10 + .../python/PythonDependencyCollector.java | 702 + .../python/PythonDependencyResolver.java | 195 + .../dependency/resolver/ruby/RubyCli.java | 30 + .../resolver/ruby/RubyDependencyResolver.java | 658 + .../dependency/resolver/sbt/IvyReport.java | 156 + .../dependency/resolver/sbt/SbtBomParser.java | 33 + .../resolver/sbt/SbtDependencyResolver.java | 344 + .../AddDependencyFileRecursionHelper.java | 13 + .../java/org/whitesource/agent/utils/Cli.java | 59 + .../agent/utils/CommandLineProcess.java | 267 + .../whitesource/agent/utils/FilesScanner.java | 155 + .../whitesource/agent/utils/FilesUtils.java | 155 + .../whitesource/agent/utils/LogContext.java | 68 + .../org/whitesource/agent/utils/LoggerFS.java | 360 + .../agent/utils/LoggerFactory.java | 17 + .../agent/utils/MemoryUsageHelper.java | 102 + .../org/whitesource/agent/utils/Pair.java | 22 + .../agent/utils/UniqueNamesGenerator.java | 27 + .../agent/utils/WsStringUtils.java | 50 + .../org/whitesource/contracts/PluginInfo.java | 7 + .../org/whitesource/fs/CommandLineArgs.java | 180 + .../org/whitesource/fs/ComponentScan.java | 118 + .../org/whitesource/fs/ExtensionUtils.java | 87 + .../whitesource/fs/FSAConfigProperties.java | 153 + .../org/whitesource/fs/FSAConfigProperty.java | 11 + .../org/whitesource/fs/FSAConfiguration.java | 1291 ++ .../org/whitesource/fs/FileSystemAgent.java | 331 + .../whitesource/fs/FileSystemAgentInfo.java | 82 + .../org/whitesource/fs/LogMapAppender.java | 37 + .../org/whitesource/fs/LogMapDefiner.java | 31 + src/main/java/org/whitesource/fs/Main.java | 292 + .../org/whitesource/fs/OfflineReader.java | 110 + .../whitesource/fs/ProjectsCalculator.java | 63 + .../org/whitesource/fs/ProjectsDetails.java | 80 + .../java/org/whitesource/fs/StatusCode.java | 34 + .../java/org/whitesource/fs/WsSecret.java | 12 + .../fs/configuration/AgentConfiguration.java | 212 + .../ConfigurationSerializer.java | 133 + .../ConfigurationValidation.java | 76 + .../configuration/EndPointConfiguration.java | 67 + .../configuration/OfflineConfiguration.java | 71 + .../RemoteDockerAWSConfiguration.java | 42 + .../RemoteDockerConfiguration.java | 151 + .../configuration/RequestConfiguration.java | 184 + .../configuration/ResolverConfiguration.java | 884 ++ .../fs/configuration/ScmConfiguration.java | 126 + .../configuration/ScmRepositoriesParser.java | 71 + .../fs/configuration/SenderConfiguration.java | 180 + .../org/whitesource/scm/GitConnector.java | 114 + .../whitesource/scm/MercurialConnector.java | 48 + .../org/whitesource/scm/ScmConnector.java | 147 + .../java/org/whitesource/scm/ScmType.java | 33 + .../org/whitesource/scm/SvnConnector.java | 89 + .../scm/passphraseCredentialsProvider.java | 131 + .../java/org/whitesource/web/FsaVerticle.java | 206 + .../java/org/whitesource/web/ResultDto.java | 47 + src/main/resources/copyDependenciesTask.txt | 10 + src/main/resources/helpContent.txt | 62 + src/main/resources/logback-FSA.xml | 47 + src/main/resources/project.properties | 3 + .../.gradle/4.0/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../4.0/fileContent/annotation-processors.bin | Bin 0 -> 18533 bytes .../.gradle/4.0/fileContent/fileContent.lock | Bin 0 -> 17 bytes .../.gradle/4.0/fileHashes/fileHashes.bin | Bin 0 -> 20147 bytes .../.gradle/4.0/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../4.0/fileHashes/resourceHashesCache.bin | Bin 0 -> 18701 bytes .../.gradle/4.0/taskHistory/fileSnapshots.bin | Bin 0 -> 24635 bytes .../.gradle/4.0/taskHistory/taskHistory.bin | Bin 0 -> 28058 bytes .../.gradle/4.0/taskHistory/taskHistory.lock | Bin 0 -> 17 bytes .../.gradle/4.5.1/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../fileContent/annotation-processors.bin | Bin 0 -> 18533 bytes .../4.5.1/fileContent/fileContent.lock | Bin 0 -> 17 bytes .../.gradle/4.5.1/fileHashes/fileHashes.bin | Bin 0 -> 19347 bytes .../.gradle/4.5.1/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../4.5.1/fileHashes/resourceHashesCache.bin | Bin 0 -> 18701 bytes .../.gradle/4.5.1/taskHistory/taskHistory.bin | Bin 0 -> 23950 bytes .../4.5.1/taskHistory/taskHistory.lock | Bin 0 -> 17 bytes .../.gradle/4.8.1/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/4.8.1/fileHashes/fileHashes.bin | Bin 0 -> 18597 bytes .../.gradle/4.8.1/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../.gradle/4.8.1/taskHistory/taskHistory.bin | Bin 0 -> 18722 bytes .../4.8.1/taskHistory/taskHistory.lock | Bin 0 -> 17 bytes .../.gradle/4.8/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../4.8/fileContent/annotation-processors.bin | Bin 0 -> 18633 bytes .../.gradle/4.8/fileContent/fileContent.lock | Bin 0 -> 17 bytes .../4.8/fileHashes/resourceHashesCache.bin | Bin 0 -> 18701 bytes .../.gradle/vcsWorkingDirs/gc.properties | 0 test_input/gradle/build.gradle | 16 + .../gradle/build/libs/elads-1.0-SNAPSHOT.jar | Bin 0 -> 1844 bytes test_input/gradle/build/tmp/jar/MANIFEST.MF | 2 + .../gradle/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54706 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + test_input/gradle/gradlew | 172 + test_input/gradle/gradlew.bat | 84 + test_input/gradle/settings.gradle | 2 + .../gradle/src/main/java/test/Hello.java | 5 + .../gradle/src/main/java/test/Hello2.java | 10 + .../gradle/src/main/java/test/Hello3.java | 8 + .../gradle/src/main/java/test/Main.java | 9 + test_input/ksa/.gitignore | 27 + test_input/ksa/LICENSE | 202 + test_input/ksa/README.md | 68 + test_input/ksa/doc/image/ksa.psd | Bin 0 -> 6567131 bytes test_input/ksa/doc/image/login-bg.jpg | Bin 0 -> 93868 bytes ...1\347\247\273\346\226\271\346\241\210.doc" | Bin 0 -> 29747 bytes test_input/ksa/ksa-core/pom.xml | 50 + .../com/ksa/context/ContextException.java | 57 + .../java/com/ksa/context/ServiceContext.java | 45 + .../com/ksa/context/ServiceContextUtils.java | 72 + .../context/spring/SpringServiceContext.java | 49 + .../java/com/ksa/dao/AbstractQueryClause.java | 95 + .../java/com/ksa/dao/DateQueryClause.java | 61 + .../java/com/ksa/dao/MultiIdsQueryClause.java | 31 + .../main/java/com/ksa/dao/QueryClause.java | 20 + .../java/com/ksa/dao/TextQueryClause.java | 29 + .../java/com/ksa/dao/bd/BasicDataDao.java | 23 + .../java/com/ksa/dao/bd/CurrencyRateDao.java | 121 + .../main/java/com/ksa/dao/bd/PartnerDao.java | 25 + .../dao/finance/AccountCurrencyRateDao.java | 21 + .../java/com/ksa/dao/finance/AccountDao.java | 28 + .../java/com/ksa/dao/finance/ChargeDao.java | 23 + .../java/com/ksa/dao/finance/InvoiceDao.java | 23 + .../logistics/AbstractLogisticsModelDao.java | 13 + .../com/ksa/dao/logistics/ArrivalNoteDao.java | 8 + .../ksa/dao/logistics/BillOfLadingDao.java | 8 + .../dao/logistics/BookingNoteCargoDao.java | 20 + .../com/ksa/dao/logistics/BookingNoteDao.java | 29 + .../com/ksa/dao/logistics/ManifestDao.java | 8 + .../dao/logistics/WarehouseBookingDao.java | 8 + .../ksa/dao/logistics/WarehouseNotingDao.java | 8 + .../com/ksa/dao/security/PermissionDao.java | 38 + .../java/com/ksa/dao/security/RoleDao.java | 72 + .../java/com/ksa/dao/security/UserDao.java | 72 + .../main/java/com/ksa/model/BaseModel.java | 41 + .../main/java/com/ksa/model/ModelState.java | 122 + .../main/java/com/ksa/model/ModelUtils.java | 88 + .../main/java/com/ksa/model/bd/BasicData.java | 104 + .../java/com/ksa/model/bd/BasicDataType.java | 104 + .../java/com/ksa/model/bd/ChargeType.java | 18 + .../main/java/com/ksa/model/bd/Currency.java | 61 + .../java/com/ksa/model/bd/CurrencyRate.java | 59 + .../main/java/com/ksa/model/bd/Partner.java | 145 + .../java/com/ksa/model/bd/PartnerType.java | 18 + .../ksa/model/business/DebitNoteCharge.java | 72 + .../ksa/model/business/RecordBillCharge.java | 195 + .../business/RecordBillChargeGather.java | 63 + .../ksa/model/business/RecordBillProfit.java | 77 + .../java/com/ksa/model/finance/Account.java | 137 + .../model/finance/AccountCurrencyRate.java | 19 + .../com/ksa/model/finance/AccountState.java | 47 + .../ksa/model/finance/BookingNoteCharge.java | 21 + .../model/finance/BookingNoteChargeState.java | 210 + .../ksa/model/finance/BookingNoteProfit.java | 84 + .../java/com/ksa/model/finance/Charge.java | 214 + .../com/ksa/model/finance/FinanceModel.java | 84 + .../java/com/ksa/model/finance/Invoice.java | 135 + .../com/ksa/model/logistics/ArrivalNote.java | 311 + .../model/logistics/BaseLogisticsModel.java | 49 + .../com/ksa/model/logistics/BillOfLading.java | 321 + .../com/ksa/model/logistics/BookingNote.java | 801 ++ .../ksa/model/logistics/BookingNoteCargo.java | 70 + .../ksa/model/logistics/BookingNoteState.java | 57 + .../com/ksa/model/logistics/Manifest.java | 258 + .../ksa/model/logistics/WarehouseBooking.java | 354 + .../ksa/model/logistics/WarehouseNoting.java | 342 + .../com/ksa/model/security/Permission.java | 39 + .../java/com/ksa/model/security/Role.java | 50 + .../java/com/ksa/model/security/User.java | 84 + .../com/ksa/service/bd/BasicDataService.java | 44 + .../ksa/service/bd/CurrencyRateService.java | 74 + .../com/ksa/service/bd/PartnerService.java | 53 + .../ksa/service/finance/AccountService.java | 62 + .../ksa/service/finance/ChargeService.java | 100 + .../ksa/service/finance/InvoiceService.java | 23 + .../service/logistics/BookingNoteService.java | 61 + .../ksa/service/security/SecurityService.java | 135 + .../src/main/java/com/ksa/util/Assert.java | 386 + .../main/java/com/ksa/util/ClassUtils.java | 1058 ++ .../java/com/ksa/util/CollectionUtils.java | 296 + .../main/java/com/ksa/util/ObjectUtils.java | 830 ++ .../java/com/ksa/util/ReflectionUtils.java | 605 + .../main/java/com/ksa/util/ResourceUtils.java | 351 + .../main/java/com/ksa/util/StringUtils.java | 1100 ++ .../main/java/com/ksa/util/codec/Base64.java | 1170 ++ .../java/com/ksa/util/codec/CharEncoding.java | 109 + test_input/ksa/ksa-dao-context/pom.xml | 40 + .../java/com/ksa/dao/AbstractMybatisDao.java | 24 + .../dao/mybatis/dialect/H2LimitDialect.java | 29 + .../ksa/dao/mybatis/dialect/LimitDialect.java | 28 + .../mybatis/dialect/MysqlLimitDialect.java | 32 + .../mybatis/dialect/OracleLimitDialect.java | 54 + .../dao/mybatis/plugin/PaginationPlugin.java | 184 + .../ksa/dao/mybatis/session/RowBounds.java | 8 + .../ksa/dao/mybatis/util/ReflectUtils.java | 99 + .../dao/mybatis/plugin/pagination.properties | 5 + .../main/resources/mybatis/mybatis-config.xml | 7 + .../main/resources/spring/dao/dao-config.xml | 30 + .../ksa/ksa-dao-root/ksa-bd-dao/pom.xml | 20 + .../dao/bd/mybatis/MybatisBasicDataDao.java | 48 + .../bd/mybatis/MybatisCurrencyRateDao.java | 132 + .../ksa/dao/bd/mybatis/MybatisPartnerDao.java | 77 + .../mybatis/mapper/bd-currency-rate.xml | 128 + .../resources/mybatis/mapper/bd-currency.xml | 27 + .../main/resources/mybatis/mapper/bd-data.xml | 103 + .../mybatis/mapper/bd-partner-extra.xml | 38 + .../mybatis/mapper/bd-partner-type.xml | 55 + .../resources/mybatis/mapper/bd-partner.xml | 132 + .../resources/spring/dao/bd-dao-context.xml | 17 + .../bd/mybatis/MybatisBasicDataDaoTest.java | 81 + .../mybatis/MybatisCurrencyRateDaoTest.java | 174 + .../dao/bd/mybatis/MybatisPartnerDaoTest.java | 138 + .../ksa-bd-dao/src/test/resources/init.sql | 509 + .../ksa/ksa-dao-root/ksa-finance-dao/pom.xml | 28 + .../MybatisAccountCurrencyRateDao.java | 37 + .../finance/mybatis/MybatisAccountDao.java | 79 + .../dao/finance/mybatis/MybatisChargeDao.java | 71 + .../finance/mybatis/MybatisInvoiceDao.java | 48 + .../mapper/finance-account-bookingnote.xml | 18 + .../mapper/finance-account-currency-rate.xml | 66 + .../mybatis/mapper/finance-account.xml | 147 + .../mybatis/mapper/finance-bookingnote.xml | 274 + .../mapper/finance-charge-bookingnote.xml | 11 + .../mybatis/mapper/finance-charge.xml | 197 + .../mybatis/mapper/finance-invoice.xml | 138 + .../mybatis/mapper/finance-profit-charge.xml | 54 + .../mybatis/mapper/finance-profit.xml | 194 + .../spring/dao/finance-dao-context.xml | 20 + .../MybatisAccountCurrencyRateDaoTest.java | 63 + .../mybatis/MybatisAccountDaoTest.java | 88 + .../finance/mybatis/MybatisChargeDaoTest.java | 106 + .../mybatis/MybatisInvoiceDaoTest.java | 98 + .../src/test/resources/init.sql | 220 + .../ksa-dao-root/ksa-logistics-dao/pom.xml | 20 + .../mybatis/MybatisArrivalNoteDao.java | 35 + .../mybatis/MybatisBillOfLadingDao.java | 35 + .../mybatis/MybatisBookingNoteCargoDao.java | 43 + .../mybatis/MybatisBookingNoteDao.java | 86 + .../logistics/mybatis/MybatisManifestDao.java | 35 + .../mybatis/MybatisWarehouseBookingDao.java | 35 + .../mybatis/MybatisWarehouseNotingDao.java | 35 + .../mybatis/mapper/logistics-arrivalnote.xml | 98 + .../mybatis/mapper/logistics-billoflading.xml | 95 + .../mapper/logistics-bookingnote-cargo.xml | 48 + .../mybatis/mapper/logistics-bookingnote.xml | 442 + .../mybatis/mapper/logistics-manifest.xml | 88 + .../mapper/logistics-warehouse-booking.xml | 108 + .../mapper/logistics-warehouse-noting.xml | 93 + .../spring/dao/logistics-dao-context.xml | 29 + .../mybatis/MybatisBillOfLadingDaoTest.java | 130 + .../mybatis/MybatisBookingNoteDaoTest.java | 354 + .../mybatis/MybatisManifestDaoTest.java | 109 + .../MybatisWarehouseBookingDaoTest.java | 146 + .../MybatisWarehouseNotingDaoTest.java | 117 + .../src/test/resources/init.sql | 352 + .../ksa/ksa-dao-root/ksa-security-dao/pom.xml | 20 + .../mybatis/MybatisPermissionDao.java | 32 + .../dao/security/mybatis/MybatisRoleDao.java | 61 + .../dao/security/mybatis/MybatisUserDao.java | 61 + .../mybatis/mapper/security-permission.xml | 69 + .../mybatis/mapper/security-role.xml | 109 + .../mybatis/mapper/security-user.xml | 112 + .../spring/dao/security-dao-context.xml | 17 + .../security/mybatis/MybatisRoleDaoTest.java | 81 + .../security/mybatis/MybatisUserDaoTest.java | 74 + .../src/test/resources/init.sql | 88 + test_input/ksa/ksa-dao-root/pom.xml | 47 + test_input/ksa/ksa-debug/pom.xml | 63 + .../main/java/com/ksa/dao/MybatisDaoTest.java | 19 + .../java/com/ksa/freemarker/TemplateTest.java | 33 + .../com/ksa/h2/H2DataSourceFactoryBean.java | 197 + .../src/main/resources/log4j.properties | 10 + .../src/main/resources/struts.properties | 2 + .../resources/test/mybatis-test-context.xml | 21 + .../ksa-service-root/ksa-bd-service/pom.xml | 24 + .../service/bd/impl/BasicDataServiceImpl.java | 69 + .../bd/impl/CurrencyRateServiceImpl.java | 207 + .../service/bd/impl/PartnerServiceImpl.java | 155 + .../ksa/service/bd/util/BasicDataUtils.java | 131 + .../spring/service/bd-service-context.xml | 20 + .../ksa-finance-service/pom.xml | 28 + .../finance/impl/AccountServiceImpl.java | 249 + .../finance/impl/ChargeServiceImpl.java | 448 + .../finance/impl/InvoiceServiceImpl.java | 114 + .../service/finance-service-context.xml | 26 + .../ksa-logistics-service/pom.xml | 24 + .../impl/BookingNoteServiceImpl.java | 299 + .../service/logistics-service-context.xml | 12 + .../ksa-security-service/pom.xml | 35 + .../security/impl/SecurityServiceImpl.java | 249 + .../security/shiro/PasswordMatcher.java | 24 + .../service/security/shiro/ShiroRealm.java | 63 + .../service/security/util/SecurityUtils.java | 87 + .../service/security-service-context.xml | 14 + test_input/ksa/ksa-service-root/pom.xml | 35 + test_input/ksa/ksa-web-core/pom.xml | 91 + .../ksa/context/web/RuntimeConfiguration.java | 38 + .../web/SpringServiceContextListener.java | 44 + .../shiro/freemarker/AuthenticatedTag.java | 45 + .../com/ksa/shiro/freemarker/GuestTag.java | 43 + .../freemarker/HasAnyPermissionsTag.java | 28 + .../ksa/shiro/freemarker/HasAnyRolesTag.java | 51 + .../shiro/freemarker/HasPermissionTag.java | 12 + .../com/ksa/shiro/freemarker/HasRoleTag.java | 10 + .../shiro/freemarker/LacksPermissionTag.java | 10 + .../ksa/shiro/freemarker/LacksRoleTag.java | 11 + .../shiro/freemarker/NotAuthenticatedTag.java | 33 + .../ksa/shiro/freemarker/PermissionTag.java | 45 + .../ksa/shiro/freemarker/PrincipalTag.java | 121 + .../com/ksa/shiro/freemarker/RoleTag.java | 28 + .../com/ksa/shiro/freemarker/SecureTag.java | 47 + .../com/ksa/shiro/freemarker/ShiroTags.java | 25 + .../com/ksa/shiro/freemarker/UserTag.java | 38 + .../ksa/shiro/web/tags/HasAnyPermissions.java | 31 + .../com/ksa/system/backup/BackupSchedule.java | 105 + .../com/ksa/system/backup/BackupTask.java | 107 + .../web/servlet/BackupScheduleListener.java | 29 + .../struts2/action/DefaultActionSupport.java | 37 + .../ksa/web/struts2/action/JsonAction.java | 12 + .../struts2/action/data/ComboDataAction.java | 12 + .../action/data/DataActionSupport.java | 32 + .../action/data/DefaultComboDataAction.java | 43 + .../action/data/DefaultGridDataAction.java | 63 + .../struts2/action/data/GridDataAction.java | 44 + .../action/data/GridDataActionSupport.java | 70 + .../struts2/action/model/GridDataModel.java | 62 + .../web/struts2/action/model/JsonResult.java | 64 + .../DataInitializedInterceptor.java | 33 + .../freemarker/FreemarkerStreamResult.java | 99 + .../freemarker/ShiroFreemarkerManager.java | 25 + .../web/wro4j/HttpServletRequestWrapper.java | 318 + .../web/wro4j/MultiXmlWroModelFactory.java | 82 + .../java/com/ksa/web/wro4j/WroFilter.java | 471 + .../web/wro4j/WroModelConfigurationCache.java | 54 + .../src/main/resources/META-INF/ksa-shiro.tld | 158 + .../src/main/resources/struts-default.xml | 65 + .../resources/struts2/struts-combodata.xml | 23 + .../resources/struts2/struts-griddata.xml | 23 + .../template/exception/service-exception.ftl | 13 + .../main/resources/template/no-permission.ftl | 11 + .../src/main/scripts/META-INF/ksa-shiro.tld | 158 + .../src/main/scripts/ksa-shiro.tld | 158 + .../ksa/ksa-web-root/ksa-bd-web/pom.xml | 33 + .../action/bd/currency/DateRateAction.java | 67 + .../action/bd/currency/PartnerRateAction.java | 26 + .../action/bd/currency/RateAction.java | 31 + .../action/bd/currency/RateSaveAction.java | 31 + .../currency/data/DateRateGridDataAction.java | 86 + .../data/PartnerRateGridDataAction.java | 55 + .../action/bd/data/BasicDataAction.java | 59 + .../action/bd/data/BasicDataDeleteAction.java | 42 + .../action/bd/data/BasicDataEditAction.java | 21 + .../action/bd/data/BasicDataInsertAction.java | 52 + .../action/bd/data/BasicDataUpdateAction.java | 56 + .../action/bd/partner/PartnerAction.java | 31 + .../bd/partner/PartnerDeleteAction.java | 42 + .../action/bd/partner/PartnerEditAction.java | 16 + .../action/bd/partner/PartnerExtraAction.java | 72 + .../bd/partner/PartnerInsertAction.java | 61 + .../bd/partner/PartnerUpdateAction.java | 65 + .../src/main/resources/js/bd/utils.js | 38 + .../src/main/resources/struts-plugin.xml | 9 + .../resources/struts2/struts-bd-component.xml | 27 + .../resources/struts2/struts-bd-currency.xml | 40 + .../main/resources/struts2/struts-bd-data.xml | 40 + .../resources/struts2/struts-bd-partner.xml | 40 + .../ui/bd/component/department-selection.ftl | 25 + .../ui/bd/component/department-selection.js | 42 + .../bd/component/partner-alias-selection.ftl | 34 + .../bd/component/partner-alias-selection.js | 120 + .../resources/ui/bd/currency/date/default.ftl | 26 + .../resources/ui/bd/currency/date/default.js | 76 + .../main/resources/ui/bd/currency/default.ftl | 15 + .../main/resources/ui/bd/currency/default.js | 8 + .../ui/bd/currency/partner/default.ftl | 24 + .../ui/bd/currency/partner/default.js | 75 + .../main/resources/ui/bd/data/create-data.ftl | 68 + .../src/main/resources/ui/bd/data/default.ftl | 39 + .../src/main/resources/ui/bd/data/default.js | 129 + .../main/resources/ui/bd/data/edit-data.ftl | 69 + .../ui/bd/partner/create-partner.ftl | 114 + .../main/resources/ui/bd/partner/default.ftl | 34 + .../main/resources/ui/bd/partner/default.js | 111 + .../resources/ui/bd/partner/edit-partner.ftl | 124 + .../main/resources/ui/bd/partner/partner.js | 104 + .../ksa-bd-web/src/main/resources/wro.xml | 10 + .../ksa/ksa-web-root/ksa-finance-web/pom.xml | 33 + .../action/finance/account/AccountAction.java | 84 + .../account/AccountCodeComputeAction.java | 55 + .../finance/account/AccountDeleteAction.java | 30 + .../account/AccountDownloadAction.java | 197 + .../finance/account/AccountEditAction.java | 49 + .../finance/account/AccountExcelAction.java | 228 + .../finance/account/AccountQueryAction.java | 95 + .../account/AccountRateGridDataAction.java | 39 + .../finance/account/AccountSaveAction.java | 38 + .../finance/account/AccountStateAction.java | 72 + .../finance/account/map/ValueGetter.java | 9 + .../AbstractBookingNoteValueGetter.java | 27 + .../map/bookingnote/AgentValueGetter.java | 21 + .../CargoContainerValueGetter.java | 18 + .../map/bookingnote/CargoNameValueGetter.java | 18 + .../bookingnote/CargoQuantityValueGetter.java | 29 + .../bookingnote/CargoVolumnValueGetter.java | 24 + .../bookingnote/CargoWeightValueGetter.java | 24 + .../map/bookingnote/CodeValueGetter.java | 18 + .../map/bookingnote/ConsigneeValueGetter.java | 21 + .../bookingnote/CreatedDateValueGetter.java | 21 + .../map/bookingnote/CreatorValueGetter.java | 21 + .../map/bookingnote/CustomerValueGetter.java | 21 + .../bookingnote/CustomsBrokerValueGetter.java | 21 + .../bookingnote/CustomsCodeValueGetter.java | 18 + .../bookingnote/CustomsDateValueGetter.java | 21 + .../bookingnote/DepartureDateValueGetter.java | 21 + .../bookingnote/DeparturePortValueGetter.java | 23 + .../DestinationDateValueGetter.java | 21 + .../DestinationPortValueGetter.java | 24 + .../map/bookingnote/InvoiceValueGetter.java | 18 + .../map/bookingnote/MawbValueGetter.java | 33 + .../map/bookingnote/RouteNameValueGetter.java | 30 + .../map/bookingnote/RouteValueGetter.java | 18 + .../map/bookingnote/SalerValueGetter.java | 21 + .../map/bookingnote/ShipperValueGetter.java | 21 + .../map/bookingnote/TypeValueGetter.java | 44 + .../finance/business/DebitNoteAction.java | 101 + .../finance/business/RecordBillAction.java | 233 + .../action/finance/charge/ChargeAction.java | 155 + .../finance/charge/ChargeEditAction.java | 75 + .../finance/charge/ChargeQueryAction.java | 179 + .../finance/charge/ChargeSaveAction.java | 27 + .../finance/charge/ChargeStateAction.java | 52 + .../charge/ForeignChargeQueryAction.java | 19 + .../charge/NativeChargeQueryAction.java | 19 + .../charge/single/ChargeSingleAction.java | 147 + .../charge/single/ChargeSingleEditAction.java | 50 + .../single/ChargeSingleQueryAction.java | 93 + .../charge/single/ChargeSingleSaveAction.java | 24 + .../single/ChargeSingleStateAction.java | 53 + .../component/FinanceSelectionAction.java | 40 + .../action/finance/invoice/InvoiceAction.java | 42 + .../finance/invoice/InvoiceAssignAction.java | 40 + .../finance/invoice/InvoiceDeleteAction.java | 33 + .../finance/invoice/InvoiceEditAction.java | 20 + .../finance/invoice/InvoiceQueryAction.java | 137 + .../finance/invoice/InvoiceSaveAction.java | 19 + .../finance/profit/ProfitQueryAction.java | 20 + .../query/AccountStateQueryClause.java | 60 + .../finance/query/ChargeStateQueryClause.java | 64 + .../query/FinanceDirectionQueryClause.java | 56 + .../query/InvoiceStateQueryClause.java | 62 + .../src/main/resources/js/finance/utils.js | 54 + .../portal/finance/account/default.ftl | 38 + .../portal/finance/account/default.js | 145 + .../portal/finance/charge/default.ftl | 23 + .../portal/finance/charge/default.js | 68 + .../src/main/resources/struts-plugin.xml | 12 + .../struts2/struts-finance-account.xml | 92 + .../struts2/struts-finance-charge-single.xml | 47 + .../struts2/struts-finance-charge.xml | 49 + .../struts2/struts-finance-component.xml | 22 + .../struts2/struts-finance-invoice.xml | 45 + .../struts2/struts-finance-profit.xml | 20 + .../struts2/struts-finance-recordbill.xml | 45 + .../ui/finance/account/account-detail.ftl | 109 + .../ui/finance/account/account-excel.ftl | 70 + .../ui/finance/account/account-excel.js | 91 + .../account/account-query-condition.js | 82 + .../finance/account/account-table-column.js | 118 + .../resources/ui/finance/account/account.css | 24 + .../resources/ui/finance/account/account.ftl | 30 + .../resources/ui/finance/account/account.js | 307 + .../resources/ui/finance/account/create.ftl | 34 + .../ui/finance/account/default-doinvoice.ftl | 46 + .../resources/ui/finance/account/default.ftl | 63 + .../resources/ui/finance/account/default.js | 267 + .../resources/ui/finance/account/edit.ftl | 27 + .../ui/finance/account/excel/account-1.ftl | 144 + .../ui/finance/account/excel/account.ftl | 60 + .../ui/finance/account/excel/account1.ftl | 223 + .../resources/ui/finance/account/invoice.ftl | 59 + .../resources/ui/finance/account/invoice.js | 101 + .../ui/finance/business/debitNote.ftl | 2400 ++++ .../ui/finance/business/recordbill-ex.ftl | 2594 ++++ .../ui/finance/business/recordbill-im.ftl | 2594 ++++ .../ui/finance/business/recordbill-ly.ftl | 2594 ++++ .../ui/finance/business/recordbill.ftl | 3794 +++++ ...215\225\350\277\233\345\217\243-3.5.2.xml" | 3320 +++++ ...215\225\350\277\233\345\217\243-3.5.3.xml" | 3203 +++++ .../charge-single/charge-single-column.js | 215 + .../charge-single/charge-single-condition.js | 78 + .../ui/finance/charge-single/checking.ftl | 39 + .../ui/finance/charge-single/checking.js | 122 + .../ui/finance/charge-single/default.ftl | 44 + .../ui/finance/charge-single/default.js | 120 + .../ui/finance/charge-single/view.css | 17 + .../ui/finance/charge-single/view.ftl | 227 + .../ui/finance/charge-single/view.js | 425 + .../charge/bookingnote-query-condition.js | 65 + .../charge/bookingnote-table-column.js | 116 + .../finance/charge/charge-query-condition.js | 102 + .../ui/finance/charge/charge-table-column.js | 185 + .../resources/ui/finance/charge/checking.ftl | 42 + .../resources/ui/finance/charge/checking.js | 130 + .../resources/ui/finance/charge/default.ftl | 47 + .../resources/ui/finance/charge/default.js | 134 + .../main/resources/ui/finance/charge/view.css | 19 + .../main/resources/ui/finance/charge/view.ftl | 256 + .../main/resources/ui/finance/charge/view.js | 601 + .../component/bookingnote-selection.ftl | 34 + .../component/bookingnote-selection.js | 71 + .../ui/finance/component/charge-selection.ftl | 32 + .../ui/finance/component/charge-selection.js | 115 + .../component/charge-template-selection.ftl | 32 + .../component/charge-template-selection.js | 71 + .../component/charge-treetable-column.js | 191 + .../finance/component/invoice-selection.ftl | 32 + .../ui/finance/component/invoice-selection.js | 75 + .../resources/ui/finance/invoice/default.ftl | 30 + .../resources/ui/finance/invoice/default.js | 93 + .../invoice/invoice-query-condition.js | 28 + .../finance/invoice/invoice-table-column.js | 80 + .../resources/ui/finance/invoice/view.css | 7 + .../resources/ui/finance/invoice/view.ftl | 130 + .../main/resources/ui/finance/invoice/view.js | 10 + .../finance/profit/profit-query-condition.js | 84 + .../ui/finance/profit/profit-table-column.js | 307 + .../ksa-web-root/ksa-logistics-web/pom.xml | 43 + .../action/logistics/DefaultAction.java | 69 + .../logistics/LogisticsModelAction.java | 48 + .../logistics/LogisticsModelDeleteAction.java | 23 + .../LogisticsModelDownloadAction.java | 58 + .../logistics/LogisticsModelSaveAction.java | 29 + .../arrivalnote/ArrivalNoteAction.java | 46 + .../arrivalnote/ArrivalNoteDeleteAction.java | 46 + .../ArrivalNoteDownloadAction.java | 53 + .../arrivalnote/ArrivalNoteSaveAction.java | 24 + .../billoflading/BillOfLadingAction.java | 64 + .../BillOfLadingCopyPoiDownloadAction.java | 12 + .../BillOfLadingDeleteAction.java | 65 + .../BillOfLadingDownloadAction.java | 70 + .../BillOfLadingPoiDownloadAction.java | 162 + .../billoflading/BillOfLadingSaveAction.java | 26 + .../bookingnote/BookingNoteAction.java | 27 + .../BookingNoteChangeTypeAction.java | 41 + .../bookingnote/BookingNoteDeleteAction.java | 41 + .../BookingNoteDownloadAction.java | 65 + .../bookingnote/BookingNoteQueryAction.java | 288 + .../BookingNoteReturnQueryAction.java | 73 + .../bookingnote/BookingNoteSaveAction.java | 86 + .../query/AccountStateQueryClause.java | 72 + .../query/BookingNoteStateQueryClause.java | 153 + .../logistics/manifest/ManifestAction.java | 45 + .../manifest/ManifestDeleteAction.java | 46 + .../manifest/ManifestSaveAction.java | 24 + .../WarehouseBookingAction.java | 47 + .../WarehouseBookingDeleteAction.java | 47 + .../WarehouseBookingSaveAction.java | 24 + .../WarehouseNotingAction.java | 24 + .../WarehouseNotingDeleteAction.java | 24 + .../WarehouseNotingDownloadAction.java | 36 + .../WarehouseNotingSaveAction.java | 24 + .../src/main/resources/js/logistics/utils.js | 18 + .../resources/portal/logistics/default.ftl | 55 + .../resources/portal/logistics/default.js | 80 + .../portal/logistics/return-notify.ftl | 25 + .../portal/logistics/return-notify.js | 52 + .../src/main/resources/struts-plugin.xml | 45 + .../resources/struts2/struts-arrivalnote.xml | 35 + .../resources/struts2/struts-billoflading.xml | 40 + .../resources/struts2/struts-bookingnote.xml | 70 + .../struts2/struts-logistics-component.xml | 12 + .../resources/struts2/struts-manifest.xml | 23 + .../struts2/struts-warehousebooking.xml | 23 + .../struts2/struts-warehousenoting.xml | 36 + .../ui/logistics/ae/arrivalnote-excel.ftl | 926 ++ .../resources/ui/logistics/ae/arrivalnote.ftl | 269 + .../resources/ui/logistics/ae/arrivalnote.js | 18 + .../logistics/ae/billoflading-copy-excel.ftl | 2413 ++++ .../ui/logistics/ae/billoflading-excel.ftl | 596 + .../ae/billoflading-original-excel.ftl | 2225 +++ .../ui/logistics/ae/billoflading.ftl | 207 + .../resources/ui/logistics/ae/billoflading.js | 49 + .../ui/logistics/ae/bookingnote-detail.ftl | 430 + .../resources/ui/logistics/ae/bookingnote.ftl | 18 + .../main/resources/ui/logistics/ae/create.ftl | 25 + .../main/resources/ui/logistics/ae/edit.ftl | 23 + .../resources/ui/logistics/ae/manifest.ftl | 110 + .../resources/ui/logistics/ae/manifest.js | 15 + .../ui/logistics/ae/warehouse-booking.ftl | 176 + .../ui/logistics/ae/warehouse-booking.js | 21 + .../logistics/ae/warehouse-noting-excel.ftl | 187 + .../ui/logistics/ae/warehouse-noting-word.ftl | 296 + .../ui/logistics/ae/warehouse-noting.ftl | 158 + .../ui/logistics/ae/warehouse-noting.js | 30 + .../ui/logistics/ai/arrivalnote-excel.ftl | 926 ++ .../resources/ui/logistics/ai/arrivalnote.ftl | 269 + .../resources/ui/logistics/ai/arrivalnote.js | 18 + .../ui/logistics/ai/bookingnote-detail.ftl | 423 + .../resources/ui/logistics/ai/bookingnote.ftl | 18 + .../main/resources/ui/logistics/ai/create.ftl | 25 + .../main/resources/ui/logistics/ai/edit.ftl | 19 + .../resources/ui/logistics/ai/manifest.ftl | 110 + .../resources/ui/logistics/ai/manifest.js | 15 + .../ui/logistics/bc/bookingnote-detail.ftl | 431 + .../resources/ui/logistics/bc/bookingnote.ftl | 18 + .../main/resources/ui/logistics/bc/create.ftl | 25 + .../main/resources/ui/logistics/bc/edit.ftl | 25 + .../logistics/bookingnote-query-condition.js | 77 + .../ui/logistics/bookingnote-table-column.js | 89 + .../resources/ui/logistics/bookingnote.css | 17 + .../resources/ui/logistics/bookingnote.js | 312 + .../ui/logistics/cc/bookingnote-detail.ftl | 431 + .../resources/ui/logistics/cc/bookingnote.ftl | 18 + .../main/resources/ui/logistics/cc/create.ftl | 25 + .../main/resources/ui/logistics/cc/edit.ftl | 25 + .../component/bookingnote-selection.ftl | 29 + .../component/bookingnote-selection.js | 71 + .../resources/ui/logistics/copy-template.js | 17 + .../main/resources/ui/logistics/default.ftl | 80 + .../main/resources/ui/logistics/default.js | 148 + .../logistics/excel/bookingnote-download.ftl | 177 + .../ui/logistics/gn/bookingnote-detail.ftl | 376 + .../resources/ui/logistics/gn/bookingnote.ftl | 18 + .../main/resources/ui/logistics/gn/create.ftl | 25 + .../main/resources/ui/logistics/gn/edit.ftl | 25 + .../ui/logistics/kb/bookingnote-detail.ftl | 431 + .../resources/ui/logistics/kb/bookingnote.ftl | 18 + .../main/resources/ui/logistics/kb/create.ftl | 25 + .../main/resources/ui/logistics/kb/edit.ftl | 25 + .../ui/logistics/ly/bookingnote-detail.ftl | 376 + .../resources/ui/logistics/ly/bookingnote.ftl | 18 + .../main/resources/ui/logistics/ly/create.ftl | 25 + .../main/resources/ui/logistics/ly/edit.ftl | 25 + .../return-notify-query-condition.js | 84 + .../logistics/return-notify-table-column.js | 115 + .../resources/ui/logistics/return-notify.ftl | 42 + .../resources/ui/logistics/return-notify.js | 83 + .../ui/logistics/rh/bookingnote-detail.ftl | 431 + .../resources/ui/logistics/rh/bookingnote.ftl | 18 + .../main/resources/ui/logistics/rh/create.ftl | 25 + .../main/resources/ui/logistics/rh/edit.ftl | 25 + .../ui/logistics/se/arrivalnote-excel.ftl | 926 ++ .../resources/ui/logistics/se/arrivalnote.ftl | 269 + .../resources/ui/logistics/se/arrivalnote.js | 18 + .../logistics/se/billoflading-copy-excel.ftl | 2413 ++++ .../ui/logistics/se/billoflading-excel.ftl | 596 + .../se/billoflading-original-excel.ftl | 2225 +++ .../ui/logistics/se/billoflading.ftl | 221 + .../resources/ui/logistics/se/billoflading.js | 55 + .../ui/logistics/se/bookingnote-detail.ftl | 439 + .../resources/ui/logistics/se/bookingnote.ftl | 18 + .../main/resources/ui/logistics/se/create.ftl | 25 + .../main/resources/ui/logistics/se/edit.ftl | 21 + .../ui/logistics/se/warehouse-booking.ftl | 174 + .../ui/logistics/se/warehouse-booking.js | 21 + .../logistics/se/warehouse-noting-excel.ftl | 187 + .../ui/logistics/se/warehouse-noting-word.ftl | 296 + .../ui/logistics/se/warehouse-noting.ftl | 158 + .../ui/logistics/se/warehouse-noting.js | 30 + .../ui/logistics/si/arrivalnote-excel.ftl | 926 ++ .../resources/ui/logistics/si/arrivalnote.ftl | 269 + .../resources/ui/logistics/si/arrivalnote.js | 18 + .../ui/logistics/si/bookingnote-detail.ftl | 439 + .../resources/ui/logistics/si/bookingnote.ftl | 18 + .../main/resources/ui/logistics/si/create.ftl | 25 + .../main/resources/ui/logistics/si/edit.ftl | 17 + .../ui/logistics/tl/bookingnote-detail.ftl | 431 + .../resources/ui/logistics/tl/bookingnote.ftl | 18 + .../main/resources/ui/logistics/tl/create.ftl | 25 + .../main/resources/ui/logistics/tl/edit.ftl | 25 + .../ui/logistics/tmpl/billoflading-copy.xls | Bin 0 -> 269824 bytes .../ui/logistics/tmpl/billoflading.xls | Bin 0 -> 267776 bytes .../ui/logistics/zj/arrivalnote-excel.ftl | 926 ++ .../resources/ui/logistics/zj/arrivalnote.ftl | 269 + .../resources/ui/logistics/zj/arrivalnote.js | 18 + .../ui/logistics/zj/bookingnote-detail.ftl | 423 + .../resources/ui/logistics/zj/bookingnote.ftl | 18 + .../main/resources/ui/logistics/zj/create.ftl | 25 + .../main/resources/ui/logistics/zj/edit.ftl | 21 + .../resources/ui/logistics/zj/manifest.ftl | 110 + .../resources/ui/logistics/zj/manifest.js | 15 + .../ksa/ksa-web-root/ksa-security-web/pom.xml | 28 + .../security/role/ClearRoleCacheAction.java | 46 + .../action/security/role/RoleAction.java | 59 + .../security/role/RoleDeleteAction.java | 41 + .../action/security/role/RoleEditAction.java | 18 + .../security/role/RoleInsertAction.java | 70 + .../role/RolePermissionDeleteAction.java | 59 + .../role/RolePermissionInsertAction.java | 59 + .../security/role/RoleUpdateAction.java | 41 + .../security/role/RoleUserDeleteAction.java | 76 + .../security/role/RoleUserInsertAction.java | 76 + .../security/user/ClearUserCacheAction.java | 24 + .../action/security/user/UserAction.java | 40 + .../security/user/UserDeleteAction.java | 42 + .../action/security/user/UserEditAction.java | 16 + .../security/user/UserInsertAction.java | 71 + .../action/security/user/UserLockAction.java | 52 + .../user/UserPasswordUpdateAction.java | 52 + .../security/user/UserRoleDeleteAction.java | 59 + .../security/user/UserRoleInsertAction.java | 59 + .../security/user/UserUpdateAction.java | 77 + .../src/main/resources/js/security/utils.js | 33 + .../src/main/resources/struts-plugin.xml | 8 + .../resources/struts2/struts-component.xml | 23 + .../main/resources/struts2/struts-role.xml | 62 + .../main/resources/struts2/struts-user.xml | 61 + .../component/permission-selection.ftl | 25 + .../component/permission-selection.js | 42 + .../ui/security/component/role-selection.ftl | 25 + .../ui/security/component/role-selection.js | 42 + .../ui/security/component/user-selection.ftl | 25 + .../ui/security/component/user-selection.js | 53 + .../ui/security/role/create-role.ftl | 48 + .../resources/ui/security/role/default.ftl | 34 + .../resources/ui/security/role/default.js | 230 + .../resources/ui/security/role/edit-role.ftl | 50 + .../resources/ui/security/role/role-action.js | 28 + .../main/resources/ui/security/role/role.js | 115 + .../ui/security/user/create-user.ftl | 85 + .../resources/ui/security/user/default.ftl | 29 + .../resources/ui/security/user/default.js | 189 + .../ui/security/user/edit-password.ftl | 44 + .../resources/ui/security/user/edit-user.ftl | 97 + .../resources/ui/security/user/user-action.js | 16 + .../main/resources/ui/security/user/user.js | 73 + .../src/main/resources/wro.xml | 10 + .../ksa-web-root/ksa-statistics-web/pom.xml | 33 + .../action/statistics/cargo/CargoAction.java | 19 + .../statistics/cargo/CargoQueryAction.java | 20 + .../action/statistics/profit/ChartAction.java | 23 + .../statistics/profit/ChartQueryAction.java | 99 + .../statistics/profit/ProfitAction.java | 19 + .../statistics/profit/model/Category.java | 26 + .../statistics/profit/model/DataSet.java | 25 + .../statistics/profit/model/DataValue.java | 26 + .../profit/model/FusionChartModel.java | 166 + .../FusionChartModelGroupByConsignee.java | 13 + .../model/FusionChartModelGroupByCreator.java | 13 + .../FusionChartModelGroupByCustomer.java | 13 + .../model/FusionChartModelGroupByDate.java | 18 + .../FusionChartModelGroupByDeparture.java | 13 + .../FusionChartModelGroupByDestination.java | 13 + .../model/FusionChartModelGroupBySaler.java | 13 + .../model/FusionChartModelGroupByShipper.java | 13 + .../model/FusionChartModelGroupByType.java | 27 + .../mybatis/mapper/statistics-cargo.xml | 167 + .../src/main/resources/struts-plugin.xml | 7 + .../struts2/struts-statistics-cargo.xml | 24 + .../struts2/struts-statistics-profit.xml | 26 + .../ui/statistics/cargo/cargo-table-column.js | 111 + .../resources/ui/statistics/cargo/default.ftl | 32 + .../resources/ui/statistics/cargo/default.js | 86 + .../resources/ui/statistics/profit/chart.ftl | 24 + .../ui/statistics/profit/data/Column2D.ftl | 3 + .../ui/statistics/profit/data/Doughnut2D.ftl | 3 + .../ui/statistics/profit/data/Error.ftl | 2 + .../ui/statistics/profit/data/Line.ftl | 3 + .../ui/statistics/profit/data/MSColumn2D.ftl | 3 + .../ui/statistics/profit/data/MSLine.ftl | 3 + .../ui/statistics/profit/data/Pie2D.ftl | 3 + .../profit/data/template/footer.ftl | 9 + .../profit/data/template/header.ftl | 4 + .../profit/data/template/multi-series.ftl | 16 + .../profit/data/template/single-series.ftl | 11 + .../ui/statistics/profit/default.ftl | 81 + .../resources/ui/statistics/profit/default.js | 168 + .../ksa/ksa-web-root/ksa-system-web/pom.xml | 43 + .../initialize/convert/BusinessConverter.java | 250 + .../initialize/convert/FinanceConverter.java | 125 + .../initialize/convert/PartnerConverter.java | 108 + .../initialize/convert/UserConverter.java | 93 + .../ksa/system/initialize/model/FeiYong.java | 106 + .../system/initialize/model/JieSuanDan.java | 78 + .../com/ksa/system/initialize/model/KeHu.java | 88 + .../ksa/system/initialize/model/TuoDan.java | 517 + .../ksa/system/initialize/model/YongHu.java | 67 + .../initialize/util/InitializeUtils.java | 37 + .../action/system/backup/BackupAction.java | 43 + .../action/system/backup/DeleteAction.java | 45 + .../action/system/backup/DownloadAction.java | 41 + .../action/system/backup/FileWrapper.java | 25 + .../action/system/backup/ManagerAction.java | 44 + .../backup/QueryBackupFileListAction.java | 75 + .../action/system/backup/RestoreAction.java | 96 + .../action/system/backup/StrategySave.java | 47 + .../action/system/backup/StrategyView.java | 66 + .../action/system/initialize/BaseAction.java | 89 + .../initialize/CheckConnectionAction.java | 29 + .../system/initialize/ExecuteSqlAction.java | 42 + .../system/initialize/InitializeAction.java | 46 + .../mybatis/initialize/init-feiyong.xml | 28 + .../resources/mybatis/initialize/init-jsd.xml | 20 + .../mybatis/initialize/init-kehu.xml | 28 + .../mybatis/initialize/init-tuodan.xml | 275 + .../mybatis/initialize/init-user.xml | 20 + .../mybatis/mapper/system-initialize.xml | 29 + .../src/main/resources/struts-plugin.xml | 7 + .../struts2/struts-system-backup.xml | 48 + .../struts2/struts-system-initialize.xml | 31 + .../resources/template/data-initialize.ftl | 110 + .../resources/ui/system/backup/default.ftl | 24 + .../resources/ui/system/backup/default.js | 105 + .../ui/system/backup/download-error.ftl | 11 + .../resources/ui/system/backup/strategy.ftl | 47 + test_input/ksa/ksa-web-root/ksa-web/pom.xml | 66 + .../src/main/resources/log4j.properties | 5 + .../src/main/resources/struts-plugin.xml | 16 + .../webapp/WEB-INF/applicationContext.xml | 39 + .../src/main/webapp/WEB-INF/decorators/bd.jsp | 35 + .../main/webapp/WEB-INF/decorators/chart.jsp | 27 + .../webapp/WEB-INF/decorators/component.jsp | 22 + .../main/webapp/WEB-INF/decorators/dialog.jsp | 23 + .../main/webapp/WEB-INF/decorators/main.jsp | 22 + .../main/webapp/WEB-INF/decorators/portal.jsp | 22 + .../main/webapp/WEB-INF/decorators/print.jsp | 46 + .../main/webapp/WEB-INF/decorators/system.jsp | 33 + .../main/webapp/WEB-INF/include/main-head.jsp | 18 + .../main/webapp/WEB-INF/include/main-nav.jsp | 174 + .../ksa-web/src/main/webapp/WEB-INF/index.jsp | 79 + .../main/webapp/WEB-INF/runtime.properties | 10 + .../ksa-web/src/main/webapp/WEB-INF/shiro.ini | 24 + .../src/main/webapp/WEB-INF/sitemesh3.xml | 44 + .../ksa-web/src/main/webapp/WEB-INF/web.xml | 79 + .../ksa-web/src/main/webapp/WEB-INF/wro.xml | 147 + .../ksa-web/src/main/webapp/favicon.ico | Bin 0 -> 65327 bytes .../ksa-web/src/main/webapp/index.jsp | 2 + .../ksa-web/src/main/webapp/init.sql | 1126 ++ .../ksa-web/src/main/webapp/login.jsp | 88 + .../ksa-web/src/main/webapp/logout.jsp | 5 + .../src/main/webapp/rs/bootstrap/LICENSE | 176 + .../src/main/webapp/rs/bootstrap/README.md | 139 + .../rs/bootstrap/css/bootstrap-responsive.css | 1040 ++ .../webapp/rs/bootstrap/css/bootstrap.css | 5624 ++++++++ .../img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../rs/bootstrap/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes .../webapp/rs/bootstrap/js/bootstrap-affix.js | 104 + .../webapp/rs/bootstrap/js/bootstrap-alert.js | 90 + .../rs/bootstrap/js/bootstrap-button.js | 96 + .../rs/bootstrap/js/bootstrap-carousel.js | 176 + .../rs/bootstrap/js/bootstrap-collapse.js | 158 + .../rs/bootstrap/js/bootstrap-dropdown.js | 150 + .../webapp/rs/bootstrap/js/bootstrap-modal.js | 239 + .../rs/bootstrap/js/bootstrap-popover.js | 103 + .../rs/bootstrap/js/bootstrap-scrollspy.js | 151 + .../webapp/rs/bootstrap/js/bootstrap-tab.js | 135 + .../rs/bootstrap/js/bootstrap-tooltip.js | 275 + .../rs/bootstrap/js/bootstrap-transition.js | 60 + .../rs/bootstrap/js/bootstrap-typeahead.js | 300 + .../main/webapp/rs/bootstrap/js/bootstrap.js | 2027 +++ .../src/main/webapp/rs/bootstrap/package.json | 25 + .../src/main/webapp/rs/charts/Column2D.swf | Bin 0 -> 58330 bytes .../src/main/webapp/rs/charts/Doughnut2D.swf | Bin 0 -> 42950 bytes .../src/main/webapp/rs/charts/Line.swf | Bin 0 -> 57883 bytes .../src/main/webapp/rs/charts/MSColumn2D.swf | Bin 0 -> 61706 bytes .../src/main/webapp/rs/charts/MSLine.swf | Bin 0 -> 61404 bytes .../src/main/webapp/rs/charts/Pie2D.swf | Bin 0 -> 42521 bytes .../custom/component/css/compositequery.css | 36 + .../custom/component/jquery.compositequery.js | 150 + .../component/jquery.multipleselection.js | 179 + .../rs/custom/easyui/locale/easyui-lang-en.js | 52 + .../rs/custom/easyui/locale/easyui-lang-fr.js | 51 + .../custom/easyui/locale/easyui-lang-zh_CN.js | 75 + .../custom/easyui/plugins/jquery.accordion.js | 280 + .../custom/easyui/plugins/jquery.calendar.js | 305 + .../rs/custom/easyui/plugins/jquery.combo.js | 518 + .../custom/easyui/plugins/jquery.combobox.js | 416 + .../custom/easyui/plugins/jquery.combogrid.js | 247 + .../custom/easyui/plugins/jquery.combotree.js | 173 + .../custom/easyui/plugins/jquery.datagrid.js | 2538 ++++ .../custom/easyui/plugins/jquery.datebox.js | 122 + .../easyui/plugins/jquery.datetimebox.js | 152 + .../rs/custom/easyui/plugins/jquery.dialog.js | 176 + .../custom/easyui/plugins/jquery.draggable.js | 249 + .../custom/easyui/plugins/jquery.droppable.js | 51 + .../rs/custom/easyui/plugins/jquery.form.js | 215 + .../rs/custom/easyui/plugins/jquery.layout.js | 697 + .../easyui/plugins/jquery.linkbutton.js | 103 + .../rs/custom/easyui/plugins/jquery.menu.js | 369 + .../easyui/plugins/jquery.menubutton.js | 105 + .../custom/easyui/plugins/jquery.messager.js | 224 + .../custom/easyui/plugins/jquery.numberbox.js | 272 + .../easyui/plugins/jquery.numberspinner.js | 67 + .../easyui/plugins/jquery.pagination.js | 263 + .../rs/custom/easyui/plugins/jquery.panel.js | 662 + .../rs/custom/easyui/plugins/jquery.parser.js | 107 + .../rs/custom/easyui/plugins/jquery.portal.js | 327 + .../easyui/plugins/jquery.progressbar.js | 77 + .../easyui/plugins/jquery.propertygrid.js | 199 + .../custom/easyui/plugins/jquery.resizable.js | 168 + .../custom/easyui/plugins/jquery.searchbox.js | 175 + .../rs/custom/easyui/plugins/jquery.slider.js | 234 + .../custom/easyui/plugins/jquery.spinner.js | 195 + .../easyui/plugins/jquery.splitbutton.js | 106 + .../rs/custom/easyui/plugins/jquery.tabs.js | 588 + .../easyui/plugins/jquery.timespinner.js | 187 + .../rs/custom/easyui/plugins/jquery.tree.js | 996 ++ .../custom/easyui/plugins/jquery.treegrid.js | 1090 ++ .../easyui/plugins/jquery.validatebox.js | 240 + .../rs/custom/easyui/plugins/jquery.window.js | 441 + .../custom/easyui/themes/gray/accordion.css | 34 + .../rs/custom/easyui/themes/gray/calendar.css | 172 + .../rs/custom/easyui/themes/gray/combo.css | 39 + .../rs/custom/easyui/themes/gray/combobox.css | 25 + .../rs/custom/easyui/themes/gray/datagrid.css | 260 + .../rs/custom/easyui/themes/gray/datebox.css | 30 + .../rs/custom/easyui/themes/gray/dialog.css | 24 + .../rs/custom/easyui/themes/gray/easyui.css | 1685 +++ .../themes/gray/images/accordion_arrows.png | Bin 0 -> 3010 bytes .../easyui/themes/gray/images/blank.gif | Bin 0 -> 43 bytes .../easyui/themes/gray/images/button_a_bg.gif | Bin 0 -> 1194 bytes .../themes/gray/images/button_plain_hover.png | Bin 0 -> 144 bytes .../themes/gray/images/button_span_bg.gif | Bin 0 -> 2061 bytes .../themes/gray/images/calendar_nextmonth.gif | Bin 0 -> 64 bytes .../themes/gray/images/calendar_nextyear.gif | Bin 0 -> 75 bytes .../themes/gray/images/calendar_prevmonth.gif | Bin 0 -> 66 bytes .../themes/gray/images/calendar_prevyear.gif | Bin 0 -> 76 bytes .../easyui/themes/gray/images/combo_arrow.gif | Bin 0 -> 82 bytes .../themes/gray/images/datagrid_header_bg.gif | Bin 0 -> 97 bytes .../gray/images/datagrid_row_collapse.gif | Bin 0 -> 80 bytes .../gray/images/datagrid_row_expand.gif | Bin 0 -> 91 bytes .../themes/gray/images/datagrid_sort_asc.gif | Bin 0 -> 830 bytes .../themes/gray/images/datagrid_sort_desc.gif | Bin 0 -> 833 bytes .../themes/gray/images/datagrid_title_bg.gif | Bin 0 -> 229 bytes .../themes/gray/images/datebox_arrow.png | Bin 0 -> 626 bytes .../themes/gray/images/layout_arrows.png | Bin 0 -> 604 bytes .../custom/easyui/themes/gray/images/menu.gif | Bin 0 -> 834 bytes .../themes/gray/images/menu_downarrow.png | Bin 0 -> 173 bytes .../themes/gray/images/menu_rightarrow.png | Bin 0 -> 3617 bytes .../easyui/themes/gray/images/menu_sep.png | Bin 0 -> 92 bytes .../gray/images/menu_split_downarrow.png | Bin 0 -> 185 bytes .../themes/gray/images/messager-error.jpg | Bin 0 -> 22058 bytes .../themes/gray/images/messager-error.png | Bin 0 -> 5240 bytes .../themes/gray/images/messager-info.png | Bin 0 -> 3732 bytes .../themes/gray/images/messager-question.jpg | Bin 0 -> 22180 bytes .../themes/gray/images/messager-question.png | Bin 0 -> 6855 bytes .../themes/gray/images/messager-success.jpg | Bin 0 -> 22000 bytes .../themes/gray/images/messager-success.png | Bin 0 -> 5099 bytes .../themes/gray/images/messager-warning.jpg | Bin 0 -> 22218 bytes .../themes/gray/images/messager-warning.png | Bin 0 -> 5577 bytes .../themes/gray/images/messager_error.gif | Bin 0 -> 1669 bytes .../themes/gray/images/messager_info.gif | Bin 0 -> 1586 bytes .../themes/gray/images/messager_question.gif | Bin 0 -> 1607 bytes .../themes/gray/images/messager_warning.gif | Bin 0 -> 1483 bytes .../themes/gray/images/pagination_first.gif | Bin 0 -> 925 bytes .../themes/gray/images/pagination_last.gif | Bin 0 -> 923 bytes .../themes/gray/images/pagination_load.png | Bin 0 -> 827 bytes .../themes/gray/images/pagination_loading.gif | Bin 0 -> 1737 bytes .../themes/gray/images/pagination_next.gif | Bin 0 -> 875 bytes .../themes/gray/images/pagination_prev.gif | Bin 0 -> 879 bytes .../themes/gray/images/pagination_tools.png | Bin 0 -> 4758 bytes .../themes/gray/images/panel_loading.gif | Bin 0 -> 1737 bytes .../easyui/themes/gray/images/panel_title.gif | Bin 0 -> 229 bytes .../gray/images/panel_tool_collapse.gif | Bin 0 -> 246 bytes .../themes/gray/images/panel_tool_expand.gif | Bin 0 -> 247 bytes .../easyui/themes/gray/images/panel_tools.gif | Bin 0 -> 737 bytes .../themes/gray/images/searchbox_button.png | Bin 0 -> 813 bytes .../themes/gray/images/slider_handle.png | Bin 0 -> 863 bytes .../themes/gray/images/spinner_arrow_down.gif | Bin 0 -> 53 bytes .../themes/gray/images/spinner_arrow_up.gif | Bin 0 -> 53 bytes .../easyui/themes/gray/images/tabs_close.gif | Bin 0 -> 829 bytes .../themes/gray/images/tabs_enabled.gif | Bin 0 -> 229 bytes .../themes/gray/images/tabs_leftarrow.png | Bin 0 -> 389 bytes .../themes/gray/images/tabs_rightarrow.png | Bin 0 -> 395 bytes .../easyui/themes/gray/images/tree_arrows.gif | Bin 0 -> 617 bytes .../easyui/themes/gray/images/tree_bg.jpg | Bin 0 -> 331 bytes .../themes/gray/images/tree_checkbox.png | Bin 0 -> 4947 bytes .../themes/gray/images/tree_checkbox_0.png | Bin 0 -> 3244 bytes .../themes/gray/images/tree_checkbox_1.png | Bin 0 -> 3777 bytes .../themes/gray/images/tree_checkbox_2.png | Bin 0 -> 3611 bytes .../easyui/themes/gray/images/tree_dnd_no.png | Bin 0 -> 605 bytes .../themes/gray/images/tree_dnd_yes.png | Bin 0 -> 492 bytes .../easyui/themes/gray/images/tree_elbow.png | Bin 0 -> 3444 bytes .../easyui/themes/gray/images/tree_file.gif | Bin 0 -> 118 bytes .../easyui/themes/gray/images/tree_folder.gif | Bin 0 -> 952 bytes .../themes/gray/images/tree_folder_open.gif | Bin 0 -> 956 bytes .../themes/gray/images/tree_loading.gif | Bin 0 -> 1737 bytes .../gray/images/validatebox_pointer.gif | Bin 0 -> 82 bytes .../gray/images/validatebox_warning.png | Bin 0 -> 921 bytes .../rs/custom/easyui/themes/gray/layout.css | 98 + .../custom/easyui/themes/gray/linkbutton.css | 74 + .../rs/custom/easyui/themes/gray/menu.css | 72 + .../custom/easyui/themes/gray/menubutton.css | 22 + .../rs/custom/easyui/themes/gray/messager.css | 85 + .../custom/easyui/themes/gray/pagination.css | 81 + .../rs/custom/easyui/themes/gray/panel.css | 94 + .../rs/custom/easyui/themes/gray/portal.css | 33 + .../custom/easyui/themes/gray/progressbar.css | 15 + .../easyui/themes/gray/propertygrid.css | 21 + .../custom/easyui/themes/gray/searchbox.css | 61 + .../rs/custom/easyui/themes/gray/slider.css | 87 + .../rs/custom/easyui/themes/gray/spinner.css | 38 + .../custom/easyui/themes/gray/splitbutton.css | 32 + .../rs/custom/easyui/themes/gray/tabs.css | 209 + .../rs/custom/easyui/themes/gray/tree.css | 176 + .../custom/easyui/themes/gray/validatebox.css | 50 + .../rs/custom/easyui/themes/gray/window.css | 78 + ...6\346\224\271\350\257\264\346\230\216.txt" | 27 + .../src/main/webapp/rs/custom/icon.css | 68 + .../src/main/webapp/rs/custom/icons/back.png | Bin 0 -> 912 bytes .../src/main/webapp/rs/custom/icons/blank.gif | Bin 0 -> 43 bytes .../main/webapp/rs/custom/icons/cancel.png | Bin 0 -> 1133 bytes .../src/main/webapp/rs/custom/icons/cut.png | Bin 0 -> 1024 bytes .../main/webapp/rs/custom/icons/edit_add.png | Bin 0 -> 1088 bytes .../webapp/rs/custom/icons/edit_remove.png | Bin 0 -> 625 bytes .../main/webapp/rs/custom/icons/filesave.png | Bin 0 -> 898 bytes .../src/main/webapp/rs/custom/icons/help.png | Bin 0 -> 1187 bytes .../src/main/webapp/rs/custom/icons/logo.png | Bin 0 -> 809 bytes .../main/webapp/rs/custom/icons/mini_add.png | Bin 0 -> 244 bytes .../main/webapp/rs/custom/icons/mini_edit.png | Bin 0 -> 161 bytes .../webapp/rs/custom/icons/mini_refresh.png | Bin 0 -> 160 bytes .../src/main/webapp/rs/custom/icons/no.png | Bin 0 -> 922 bytes .../src/main/webapp/rs/custom/icons/ok.png | Bin 0 -> 883 bytes .../main/webapp/rs/custom/icons/pencil.png | Bin 0 -> 713 bytes .../src/main/webapp/rs/custom/icons/print.png | Bin 0 -> 1057 bytes .../src/main/webapp/rs/custom/icons/redo.png | Bin 0 -> 708 bytes .../main/webapp/rs/custom/icons/reload.png | Bin 0 -> 1045 bytes .../main/webapp/rs/custom/icons/search.png | Bin 0 -> 813 bytes .../src/main/webapp/rs/custom/icons/sum.png | Bin 0 -> 289 bytes .../src/main/webapp/rs/custom/icons/tip.png | Bin 0 -> 743 bytes .../src/main/webapp/rs/custom/icons/undo.png | Bin 0 -> 707 bytes .../custom/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../src/main/webapp/rs/custom/img/loading.gif | Bin 0 -> 1436 bytes .../main/webapp/rs/custom/img/navbar-bg.png | Bin 0 -> 2848 bytes .../ksa-web/src/main/webapp/rs/custom/ksa.css | 235 + .../ksa-web/src/main/webapp/rs/custom/ksa.js | 138 + .../main/webapp/rs/custom/print/blueprint.css | 129 + .../main/webapp/rs/custom/print/custom.css | 51 + .../src/main/webapp/rs/fusion-charts.js | 301 + .../ksa-web/src/main/webapp/rs/html5.js | 3 + .../main/webapp/rs/images/checkbox-check.png | Bin 0 -> 3741 bytes .../src/main/webapp/rs/images/checkbox.png | Bin 0 -> 3161 bytes .../src/main/webapp/rs/images/login-bg.jpg | Bin 0 -> 96448 bytes .../src/main/webapp/rs/images/logo-banner.png | Bin 0 -> 16158 bytes .../rs/images/report-banner-arrival.png | Bin 0 -> 27982 bytes .../main/webapp/rs/images/report-banner.png | Bin 0 -> 20385 bytes .../src/main/webapp/rs/images/type/AE.png | Bin 0 -> 11966 bytes .../src/main/webapp/rs/images/type/AI.png | Bin 0 -> 11968 bytes .../src/main/webapp/rs/images/type/SE.png | Bin 0 -> 21283 bytes .../src/main/webapp/rs/images/type/SI.png | Bin 0 -> 21283 bytes .../main/webapp/rs/jquery/jquery-1.7.2.min.js | 4 + .../webapp/rs/jquery/jquery.easyui.min.js | 11500 ++++++++++++++++ .../main/webapp/rs/jquery/jquery.hotkeys.js | 99 + .../ksa-web/src/main/webapp/rs/kibo.js | 283 + .../ksa-web/src/main/webapp/rs/readme.txt | 16 + .../webapp/template/simple/actionerror.ftl | 35 + .../webapp/template/simple/actionmessage.ftl | 35 + test_input/ksa/ksa-web-root/pom.xml | 161 + test_input/ksa/mvn-update-version.sh | 7 + test_input/ksa/pom.xml | 255 + test_input/whitesource-fs-agent.config | 76 + 1138 files changed, 162632 insertions(+) create mode 100644 .gitignore create mode 100644 .whitesource create mode 100644 LICENSE.txt create mode 100644 README.EUA.md create mode 100644 README.md create mode 100644 folder-offline-test/whitesource/update-request.txt create mode 100644 pom.xml create mode 100644 shiftleft.json create mode 100644 src/main/java/org/whitesource/agent/ConfigPropertyKeys.java create mode 100644 src/main/java/org/whitesource/agent/Constants.java create mode 100644 src/main/java/org/whitesource/agent/DependencyCalculator.java create mode 100644 src/main/java/org/whitesource/agent/DependencyInfoFactory.java create mode 100644 src/main/java/org/whitesource/agent/FileSystemScanner.java create mode 100644 src/main/java/org/whitesource/agent/ProjectConfiguration.java create mode 100644 src/main/java/org/whitesource/agent/ProjectsSender.java create mode 100644 src/main/java/org/whitesource/agent/SingleFileScanner.java create mode 100644 src/main/java/org/whitesource/agent/TempFolders.java create mode 100644 src/main/java/org/whitesource/agent/ViaComponents.java create mode 100644 src/main/java/org/whitesource/agent/ViaLanguage.java create mode 100644 src/main/java/org/whitesource/agent/archive/ArchiveExtractor.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/AbstractDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/BomFile.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/BomParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/DependencyCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/DependencyResolutionService.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/IBomParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/ResolutionResult.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/ResolvedFolder.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/ViaMultiModuleAnalyzer.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerBomParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerLsJsonDependencyCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/AbstractParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/AlpineParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/ArchLinuxParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/DebianParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerImage.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/Package.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/RpmParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDocker.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDockerImage.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/RemoteDockersManager.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/DockerImageAmazon.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/RemoteDockerAmazonECR.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureCli.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureDockerImage.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureRemoteDocker.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetRestoreCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/dotNet/RestoreCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyManager.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleCli.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleLinesParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleMvnCommand.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/hex/HexDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/html/HtmlDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenLinesParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenPomParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenTreeDependencyCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmBomParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmLsJsonDependencyCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/npm/RegistryType.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/npm/YarnDependencyCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetRestoreCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetConfigFileType.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojItemGroup.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojPackages.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackage.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackageInterface.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackages.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackagesConfigXmlParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/PackageReference.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/ReferenceTag.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/packageManger/LinuxPkgManagerCommand.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/packageManger/PackageManagerExtractor.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/php/PhpDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PackageSource.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpModel.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpPackage.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/python/DependenciesFileType.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyCollector.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyCli.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/sbt/IvyReport.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtBomParser.java create mode 100644 src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtDependencyResolver.java create mode 100644 src/main/java/org/whitesource/agent/utils/AddDependencyFileRecursionHelper.java create mode 100644 src/main/java/org/whitesource/agent/utils/Cli.java create mode 100644 src/main/java/org/whitesource/agent/utils/CommandLineProcess.java create mode 100644 src/main/java/org/whitesource/agent/utils/FilesScanner.java create mode 100644 src/main/java/org/whitesource/agent/utils/FilesUtils.java create mode 100644 src/main/java/org/whitesource/agent/utils/LogContext.java create mode 100644 src/main/java/org/whitesource/agent/utils/LoggerFS.java create mode 100644 src/main/java/org/whitesource/agent/utils/LoggerFactory.java create mode 100644 src/main/java/org/whitesource/agent/utils/MemoryUsageHelper.java create mode 100644 src/main/java/org/whitesource/agent/utils/Pair.java create mode 100644 src/main/java/org/whitesource/agent/utils/UniqueNamesGenerator.java create mode 100644 src/main/java/org/whitesource/agent/utils/WsStringUtils.java create mode 100644 src/main/java/org/whitesource/contracts/PluginInfo.java create mode 100644 src/main/java/org/whitesource/fs/CommandLineArgs.java create mode 100644 src/main/java/org/whitesource/fs/ComponentScan.java create mode 100644 src/main/java/org/whitesource/fs/ExtensionUtils.java create mode 100644 src/main/java/org/whitesource/fs/FSAConfigProperties.java create mode 100644 src/main/java/org/whitesource/fs/FSAConfigProperty.java create mode 100644 src/main/java/org/whitesource/fs/FSAConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/FileSystemAgent.java create mode 100644 src/main/java/org/whitesource/fs/FileSystemAgentInfo.java create mode 100644 src/main/java/org/whitesource/fs/LogMapAppender.java create mode 100644 src/main/java/org/whitesource/fs/LogMapDefiner.java create mode 100644 src/main/java/org/whitesource/fs/Main.java create mode 100644 src/main/java/org/whitesource/fs/OfflineReader.java create mode 100644 src/main/java/org/whitesource/fs/ProjectsCalculator.java create mode 100644 src/main/java/org/whitesource/fs/ProjectsDetails.java create mode 100644 src/main/java/org/whitesource/fs/StatusCode.java create mode 100644 src/main/java/org/whitesource/fs/WsSecret.java create mode 100644 src/main/java/org/whitesource/fs/configuration/AgentConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/ConfigurationSerializer.java create mode 100644 src/main/java/org/whitesource/fs/configuration/ConfigurationValidation.java create mode 100644 src/main/java/org/whitesource/fs/configuration/EndPointConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/OfflineConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/RemoteDockerAWSConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/RemoteDockerConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/RequestConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/ResolverConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/ScmConfiguration.java create mode 100644 src/main/java/org/whitesource/fs/configuration/ScmRepositoriesParser.java create mode 100644 src/main/java/org/whitesource/fs/configuration/SenderConfiguration.java create mode 100644 src/main/java/org/whitesource/scm/GitConnector.java create mode 100644 src/main/java/org/whitesource/scm/MercurialConnector.java create mode 100644 src/main/java/org/whitesource/scm/ScmConnector.java create mode 100644 src/main/java/org/whitesource/scm/ScmType.java create mode 100644 src/main/java/org/whitesource/scm/SvnConnector.java create mode 100644 src/main/java/org/whitesource/scm/passphraseCredentialsProvider.java create mode 100644 src/main/java/org/whitesource/web/FsaVerticle.java create mode 100644 src/main/java/org/whitesource/web/ResultDto.java create mode 100644 src/main/resources/copyDependenciesTask.txt create mode 100644 src/main/resources/helpContent.txt create mode 100644 src/main/resources/logback-FSA.xml create mode 100644 src/main/resources/project.properties create mode 100644 test_input/gradle/.gradle/4.0/fileChanges/last-build.bin create mode 100644 test_input/gradle/.gradle/4.0/fileContent/annotation-processors.bin create mode 100644 test_input/gradle/.gradle/4.0/fileContent/fileContent.lock create mode 100644 test_input/gradle/.gradle/4.0/fileHashes/fileHashes.bin create mode 100644 test_input/gradle/.gradle/4.0/fileHashes/fileHashes.lock create mode 100644 test_input/gradle/.gradle/4.0/fileHashes/resourceHashesCache.bin create mode 100644 test_input/gradle/.gradle/4.0/taskHistory/fileSnapshots.bin create mode 100644 test_input/gradle/.gradle/4.0/taskHistory/taskHistory.bin create mode 100644 test_input/gradle/.gradle/4.0/taskHistory/taskHistory.lock create mode 100644 test_input/gradle/.gradle/4.5.1/fileChanges/last-build.bin create mode 100644 test_input/gradle/.gradle/4.5.1/fileContent/annotation-processors.bin create mode 100644 test_input/gradle/.gradle/4.5.1/fileContent/fileContent.lock create mode 100644 test_input/gradle/.gradle/4.5.1/fileHashes/fileHashes.bin create mode 100644 test_input/gradle/.gradle/4.5.1/fileHashes/fileHashes.lock create mode 100644 test_input/gradle/.gradle/4.5.1/fileHashes/resourceHashesCache.bin create mode 100644 test_input/gradle/.gradle/4.5.1/taskHistory/taskHistory.bin create mode 100644 test_input/gradle/.gradle/4.5.1/taskHistory/taskHistory.lock create mode 100644 test_input/gradle/.gradle/4.8.1/fileChanges/last-build.bin create mode 100644 test_input/gradle/.gradle/4.8.1/fileHashes/fileHashes.bin create mode 100644 test_input/gradle/.gradle/4.8.1/fileHashes/fileHashes.lock create mode 100644 test_input/gradle/.gradle/4.8.1/taskHistory/taskHistory.bin create mode 100644 test_input/gradle/.gradle/4.8.1/taskHistory/taskHistory.lock create mode 100644 test_input/gradle/.gradle/4.8/fileChanges/last-build.bin create mode 100644 test_input/gradle/.gradle/4.8/fileContent/annotation-processors.bin create mode 100644 test_input/gradle/.gradle/4.8/fileContent/fileContent.lock create mode 100644 test_input/gradle/.gradle/4.8/fileHashes/resourceHashesCache.bin create mode 100644 test_input/gradle/.gradle/vcsWorkingDirs/gc.properties create mode 100644 test_input/gradle/build.gradle create mode 100644 test_input/gradle/build/libs/elads-1.0-SNAPSHOT.jar create mode 100644 test_input/gradle/build/tmp/jar/MANIFEST.MF create mode 100644 test_input/gradle/gradle/wrapper/gradle-wrapper.jar create mode 100644 test_input/gradle/gradle/wrapper/gradle-wrapper.properties create mode 100644 test_input/gradle/gradlew create mode 100644 test_input/gradle/gradlew.bat create mode 100644 test_input/gradle/settings.gradle create mode 100644 test_input/gradle/src/main/java/test/Hello.java create mode 100644 test_input/gradle/src/main/java/test/Hello2.java create mode 100644 test_input/gradle/src/main/java/test/Hello3.java create mode 100644 test_input/gradle/src/main/java/test/Main.java create mode 100644 test_input/ksa/.gitignore create mode 100644 test_input/ksa/LICENSE create mode 100644 test_input/ksa/README.md create mode 100644 test_input/ksa/doc/image/ksa.psd create mode 100644 test_input/ksa/doc/image/login-bg.jpg create mode 100644 "test_input/ksa/doc/\346\235\255\345\267\236\345\207\257\346\200\235\347\210\261\347\211\251\346\265\201\347\256\241\347\220\206\347\263\273\347\273\237\350\277\201\347\247\273\346\226\271\346\241\210.doc" create mode 100644 test_input/ksa/ksa-core/pom.xml create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/context/ContextException.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContext.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContextUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/context/spring/SpringServiceContext.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/AbstractQueryClause.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/DateQueryClause.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/MultiIdsQueryClause.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/QueryClause.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/TextQueryClause.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/BasicDataDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/CurrencyRateDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/PartnerDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountCurrencyRateDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/ChargeDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/InvoiceDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/AbstractLogisticsModelDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ArrivalNoteDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BillOfLadingDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteCargoDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ManifestDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseBookingDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseNotingDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/PermissionDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/RoleDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/UserDao.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/BaseModel.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelState.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicData.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicDataType.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/ChargeType.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Currency.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/CurrencyRate.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Partner.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/PartnerType.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/DebitNoteCharge.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillCharge.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillChargeGather.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillProfit.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Account.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountCurrencyRate.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountState.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteCharge.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteChargeState.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteProfit.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Charge.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/FinanceModel.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Invoice.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/ArrivalNote.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BaseLogisticsModel.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BillOfLading.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNote.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteCargo.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteState.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/Manifest.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseBooking.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseNoting.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Permission.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Role.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/User.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/BasicDataService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/CurrencyRateService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/PartnerService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/AccountService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/ChargeService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/InvoiceService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/logistics/BookingNoteService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/service/security/SecurityService.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/Assert.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/ClassUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/CollectionUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/ObjectUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/ReflectionUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/ResourceUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/StringUtils.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/Base64.java create mode 100644 test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/CharEncoding.java create mode 100644 test_input/ksa/ksa-dao-context/pom.xml create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/AbstractMybatisDao.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/H2LimitDialect.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/LimitDialect.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/MysqlLimitDialect.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/OracleLimitDialect.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/plugin/PaginationPlugin.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/session/RowBounds.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/util/ReflectUtils.java create mode 100644 test_input/ksa/ksa-dao-context/src/main/resources/com/ksa/dao/mybatis/plugin/pagination.properties create mode 100644 test_input/ksa/ksa-dao-context/src/main/resources/mybatis/mybatis-config.xml create mode 100644 test_input/ksa/ksa-dao-context/src/main/resources/spring/dao/dao-config.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/pom.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisPartnerDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency-rate.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-data.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-extra.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-type.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/spring/dao/bd-dao-context.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisPartnerDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/resources/init.sql create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/pom.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisChargeDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-bookingnote.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-currency-rate.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-bookingnote.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge-bookingnote.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-invoice.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit-charge.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/spring/dao/finance-dao-context.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisChargeDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/resources/init.sql create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/pom.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisArrivalNoteDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteCargoDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisManifestDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-arrivalnote.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-billoflading.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote-cargo.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-manifest.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-booking.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-noting.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/spring/dao/logistics-dao-context.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisManifestDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/resources/init.sql create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/pom.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisPermissionDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisRoleDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisUserDao.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-permission.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-role.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-user.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/spring/dao/security-dao-context.xml create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisRoleDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisUserDaoTest.java create mode 100644 test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/resources/init.sql create mode 100644 test_input/ksa/ksa-dao-root/pom.xml create mode 100644 test_input/ksa/ksa-debug/pom.xml create mode 100644 test_input/ksa/ksa-debug/src/main/java/com/ksa/dao/MybatisDaoTest.java create mode 100644 test_input/ksa/ksa-debug/src/main/java/com/ksa/freemarker/TemplateTest.java create mode 100644 test_input/ksa/ksa-debug/src/main/java/com/ksa/h2/H2DataSourceFactoryBean.java create mode 100644 test_input/ksa/ksa-debug/src/main/resources/log4j.properties create mode 100644 test_input/ksa/ksa-debug/src/main/resources/struts.properties create mode 100644 test_input/ksa/ksa-debug/src/main/resources/test/mybatis-test-context.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-bd-service/pom.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/BasicDataServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/CurrencyRateServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/PartnerServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/util/BasicDataUtils.java create mode 100644 test_input/ksa/ksa-service-root/ksa-bd-service/src/main/resources/spring/service/bd-service-context.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-finance-service/pom.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/AccountServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/ChargeServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/InvoiceServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-finance-service/src/main/resources/spring/service/finance-service-context.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-logistics-service/pom.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/java/com/ksa/service/logistics/impl/BookingNoteServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/resources/spring/service/logistics-service-context.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-security-service/pom.xml create mode 100644 test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/impl/SecurityServiceImpl.java create mode 100644 test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/PasswordMatcher.java create mode 100644 test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/ShiroRealm.java create mode 100644 test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/util/SecurityUtils.java create mode 100644 test_input/ksa/ksa-service-root/ksa-security-service/src/main/resources/spring/service/security-service-context.xml create mode 100644 test_input/ksa/ksa-service-root/pom.xml create mode 100644 test_input/ksa/ksa-web-core/pom.xml create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/RuntimeConfiguration.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/SpringServiceContextListener.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/AuthenticatedTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/GuestTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyPermissionsTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyRolesTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasPermissionTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasRoleTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksPermissionTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksRoleTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/NotAuthenticatedTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PermissionTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PrincipalTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/RoleTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/SecureTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/ShiroTags.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/UserTag.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/web/tags/HasAnyPermissions.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupSchedule.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupTask.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/servlet/BackupScheduleListener.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/DefaultActionSupport.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/JsonAction.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/ComboDataAction.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DataActionSupport.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultComboDataAction.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultGridDataAction.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataAction.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataActionSupport.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/GridDataModel.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/JsonResult.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/interceptor/DataInitializedInterceptor.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/FreemarkerStreamResult.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/ShiroFreemarkerManager.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/HttpServletRequestWrapper.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/MultiXmlWroModelFactory.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroFilter.java create mode 100644 test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroModelConfigurationCache.java create mode 100644 test_input/ksa/ksa-web-core/src/main/resources/META-INF/ksa-shiro.tld create mode 100644 test_input/ksa/ksa-web-core/src/main/resources/struts-default.xml create mode 100644 test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-combodata.xml create mode 100644 test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-griddata.xml create mode 100644 test_input/ksa/ksa-web-core/src/main/resources/template/exception/service-exception.ftl create mode 100644 test_input/ksa/ksa-web-core/src/main/resources/template/no-permission.ftl create mode 100644 test_input/ksa/ksa-web-core/src/main/scripts/META-INF/ksa-shiro.tld create mode 100644 test_input/ksa/ksa-web-core/src/main/scripts/ksa-shiro.tld create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/pom.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/DateRateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/PartnerRateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/DateRateGridDataAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/PartnerRateGridDataAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataInsertAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataUpdateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerExtraAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerInsertAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerUpdateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/js/bd/utils.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts-plugin.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-component.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-currency.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-data.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-partner.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/partner-alias-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/partner-alias-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/create-data.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/edit-data.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/create-partner.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/edit-partner.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/partner.js create mode 100644 test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/wro.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/pom.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountCodeComputeAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountExcelAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountRateGridDataAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountStateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/ValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AbstractBookingNoteValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AgentValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoContainerValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoNameValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoQuantityValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoVolumnValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoWeightValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CodeValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ConsigneeValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatedDateValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatorValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomerValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsBrokerValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsCodeValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsDateValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DepartureDateValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DeparturePortValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationDateValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationPortValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/InvoiceValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/MawbValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteNameValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/SalerValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ShipperValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/TypeValueGetter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/DebitNoteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/RecordBillAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeStateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ForeignChargeQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/NativeChargeQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleStateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/component/FinanceSelectionAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAssignAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/profit/ProfitQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/AccountStateQueryClause.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/ChargeStateQueryClause.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/FinanceDirectionQueryClause.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/InvoiceStateQueryClause.java create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/js/finance/utils.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts-plugin.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-account.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge-single.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-component.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-invoice.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-profit.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-recordbill.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-query-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.css create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default-doinvoice.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account-1.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account1.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/debitNote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/recordbill-ex.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/recordbill-im.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/recordbill-ly.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/recordbill.ftl create mode 100644 "test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/\351\235\242\345\215\225\350\277\233\345\217\243-3.5.2.xml" create mode 100644 "test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/\351\235\242\345\215\225\350\277\233\345\217\243-3.5.3.xml" create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/charge-single-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/charge-single-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/checking.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/checking.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/view.css create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/view.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge-single/view.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/bookingnote-query-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/bookingnote-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/charge-query-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/charge-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/checking.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/checking.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/view.css create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/view.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/charge/view.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/bookingnote-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/bookingnote-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/charge-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/charge-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/charge-template-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/charge-template-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/charge-treetable-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/invoice-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/component/invoice-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/invoice/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/invoice/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/invoice/invoice-query-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/invoice/invoice-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/invoice/view.css create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/invoice/view.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/invoice/view.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/profit/profit-query-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/profit/profit-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/pom.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/DefaultAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/LogisticsModelAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/LogisticsModelDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/LogisticsModelDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/LogisticsModelSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/arrivalnote/ArrivalNoteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/arrivalnote/ArrivalNoteDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/arrivalnote/ArrivalNoteDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/arrivalnote/ArrivalNoteSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/billoflading/BillOfLadingAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/billoflading/BillOfLadingCopyPoiDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/billoflading/BillOfLadingDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/billoflading/BillOfLadingDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/billoflading/BillOfLadingPoiDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/billoflading/BillOfLadingSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/BookingNoteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/BookingNoteChangeTypeAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/BookingNoteDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/BookingNoteDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/BookingNoteQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/BookingNoteReturnQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/BookingNoteSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/query/AccountStateQueryClause.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/bookingnote/query/BookingNoteStateQueryClause.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/manifest/ManifestAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/manifest/ManifestDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/manifest/ManifestSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/warehousebooking/WarehouseBookingAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/warehousebooking/WarehouseBookingDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/warehousebooking/WarehouseBookingSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/warehousenoting/WarehouseNotingAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/warehousenoting/WarehouseNotingDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/warehousenoting/WarehouseNotingDownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/java/com/ksa/web/struts2/action/logistics/warehousenoting/WarehouseNotingSaveAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/js/logistics/utils.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/portal/logistics/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/portal/logistics/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/portal/logistics/return-notify.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/portal/logistics/return-notify.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts-plugin.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts2/struts-arrivalnote.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts2/struts-billoflading.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts2/struts-bookingnote.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts2/struts-logistics-component.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts2/struts-manifest.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts2/struts-warehousebooking.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/struts2/struts-warehousenoting.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/arrivalnote-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/arrivalnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/arrivalnote.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/billoflading-copy-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/billoflading-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/billoflading-original-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/billoflading.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/billoflading.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/manifest.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/manifest.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/warehouse-booking.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/warehouse-booking.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/warehouse-noting-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/warehouse-noting-word.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/warehouse-noting.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ae/warehouse-noting.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/arrivalnote-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/arrivalnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/arrivalnote.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/manifest.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ai/manifest.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bc/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bc/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bc/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bc/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bookingnote-query-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bookingnote-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bookingnote.css create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/bookingnote.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/cc/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/cc/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/cc/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/cc/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/component/bookingnote-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/component/bookingnote-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/copy-template.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/excel/bookingnote-download.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/gn/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/gn/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/gn/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/gn/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/kb/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/kb/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/kb/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/kb/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ly/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ly/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ly/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/ly/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/return-notify-query-condition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/return-notify-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/return-notify.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/return-notify.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/rh/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/rh/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/rh/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/rh/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/arrivalnote-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/arrivalnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/arrivalnote.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/billoflading-copy-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/billoflading-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/billoflading-original-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/billoflading.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/billoflading.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/warehouse-booking.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/warehouse-booking.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/warehouse-noting-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/warehouse-noting-word.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/warehouse-noting.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/se/warehouse-noting.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/si/arrivalnote-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/si/arrivalnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/si/arrivalnote.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/si/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/si/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/si/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/si/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/tl/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/tl/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/tl/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/tl/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/tmpl/billoflading-copy.xls create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/tmpl/billoflading.xls create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/arrivalnote-excel.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/arrivalnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/arrivalnote.js create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/bookingnote-detail.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/bookingnote.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/create.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/edit.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/manifest.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-logistics-web/src/main/resources/ui/logistics/zj/manifest.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/pom.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/ClearRoleCacheAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RoleAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RoleDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RoleEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RoleInsertAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RolePermissionDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RolePermissionInsertAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RoleUpdateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RoleUserDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/role/RoleUserInsertAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/ClearUserCacheAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserEditAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserInsertAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserLockAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserPasswordUpdateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserRoleDeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserRoleInsertAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/java/com/ksa/web/struts2/action/security/user/UserUpdateAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/js/security/utils.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/struts-plugin.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/struts2/struts-component.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/struts2/struts-role.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/struts2/struts-user.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/component/permission-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/component/permission-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/component/role-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/component/role-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/component/user-selection.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/component/user-selection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/role/create-role.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/role/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/role/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/role/edit-role.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/role/role-action.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/role/role.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/user/create-user.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/user/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/user/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/user/edit-password.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/user/edit-user.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/user/user-action.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/ui/security/user/user.js create mode 100644 test_input/ksa/ksa-web-root/ksa-security-web/src/main/resources/wro.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/pom.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/cargo/CargoAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/cargo/CargoQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/ChartAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/ChartQueryAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/ProfitAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/Category.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/DataSet.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/DataValue.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModel.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByConsignee.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByCreator.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByCustomer.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByDate.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByDeparture.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByDestination.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupBySaler.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByShipper.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/java/com/ksa/web/struts2/action/statistics/profit/model/FusionChartModelGroupByType.java create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/mybatis/mapper/statistics-cargo.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/struts-plugin.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/struts2/struts-statistics-cargo.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/struts2/struts-statistics-profit.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/cargo/cargo-table-column.js create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/cargo/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/cargo/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/chart.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/Column2D.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/Doughnut2D.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/Error.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/Line.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/MSColumn2D.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/MSLine.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/Pie2D.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/template/footer.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/template/header.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/template/multi-series.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/data/template/single-series.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-statistics-web/src/main/resources/ui/statistics/profit/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/pom.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/convert/BusinessConverter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/convert/FinanceConverter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/convert/PartnerConverter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/convert/UserConverter.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/model/FeiYong.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/model/JieSuanDan.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/model/KeHu.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/model/TuoDan.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/model/YongHu.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/system/initialize/util/InitializeUtils.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/BackupAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/DeleteAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/DownloadAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/FileWrapper.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/ManagerAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/QueryBackupFileListAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/RestoreAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/StrategySave.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/backup/StrategyView.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/initialize/BaseAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/initialize/CheckConnectionAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/initialize/ExecuteSqlAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/java/com/ksa/web/struts2/action/system/initialize/InitializeAction.java create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/mybatis/initialize/init-feiyong.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/mybatis/initialize/init-jsd.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/mybatis/initialize/init-kehu.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/mybatis/initialize/init-tuodan.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/mybatis/initialize/init-user.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/mybatis/mapper/system-initialize.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/struts-plugin.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/struts2/struts-system-backup.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/struts2/struts-system-initialize.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/template/data-initialize.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/ui/system/backup/default.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/ui/system/backup/default.js create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/ui/system/backup/download-error.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-system-web/src/main/resources/ui/system/backup/strategy.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-web/pom.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/resources/log4j.properties create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/resources/struts-plugin.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/applicationContext.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/bd.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/chart.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/component.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/dialog.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/main.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/portal.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/print.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/decorators/system.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/include/main-head.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/include/main-nav.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/index.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/runtime.properties create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/shiro.ini create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/sitemesh3.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/web.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/WEB-INF/wro.xml create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/favicon.ico create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/index.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/init.sql create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/login.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/logout.jsp create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/LICENSE create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/README.md create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/css/bootstrap-responsive.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/css/bootstrap.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/img/glyphicons-halflings-white.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/img/glyphicons-halflings.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-affix.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-alert.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-button.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-carousel.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-collapse.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-dropdown.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-modal.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-popover.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-scrollspy.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-tab.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-tooltip.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-transition.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap-typeahead.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/js/bootstrap.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/bootstrap/package.json create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/charts/Column2D.swf create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/charts/Doughnut2D.swf create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/charts/Line.swf create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/charts/MSColumn2D.swf create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/charts/MSLine.swf create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/charts/Pie2D.swf create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/component/css/compositequery.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/component/jquery.compositequery.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/component/jquery.multipleselection.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/locale/easyui-lang-en.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/locale/easyui-lang-fr.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/locale/easyui-lang-zh_CN.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.accordion.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.calendar.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.combo.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.combobox.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.combogrid.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.combotree.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.datagrid.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.datebox.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.datetimebox.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.dialog.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.draggable.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.droppable.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.form.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.layout.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.linkbutton.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.menu.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.menubutton.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.messager.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.numberbox.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.numberspinner.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.pagination.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.panel.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.parser.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.portal.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.progressbar.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.propertygrid.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.resizable.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.searchbox.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.slider.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.spinner.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.splitbutton.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.tabs.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.timespinner.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.tree.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.treegrid.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.validatebox.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/plugins/jquery.window.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/accordion.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/calendar.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/combo.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/combobox.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/datagrid.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/datebox.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/dialog.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/easyui.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/accordion_arrows.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/blank.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/button_a_bg.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/button_plain_hover.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/button_span_bg.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/calendar_nextmonth.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/calendar_nextyear.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/calendar_prevmonth.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/calendar_prevyear.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/combo_arrow.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/datagrid_header_bg.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/datagrid_row_collapse.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/datagrid_row_expand.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/datagrid_sort_asc.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/datagrid_sort_desc.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/datagrid_title_bg.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/datebox_arrow.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/layout_arrows.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/menu.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/menu_downarrow.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/menu_rightarrow.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/menu_sep.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/menu_split_downarrow.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-error.jpg create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-error.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-info.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-question.jpg create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-question.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-success.jpg create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-success.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-warning.jpg create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager-warning.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager_error.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager_info.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager_question.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/messager_warning.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/pagination_first.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/pagination_last.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/pagination_load.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/pagination_loading.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/pagination_next.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/pagination_prev.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/pagination_tools.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/panel_loading.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/panel_title.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/panel_tool_collapse.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/panel_tool_expand.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/panel_tools.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/searchbox_button.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/slider_handle.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/spinner_arrow_down.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/spinner_arrow_up.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tabs_close.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tabs_enabled.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tabs_leftarrow.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tabs_rightarrow.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_arrows.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_bg.jpg create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_checkbox.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_checkbox_0.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_checkbox_1.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_checkbox_2.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_dnd_no.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_dnd_yes.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_elbow.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_file.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_folder.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_folder_open.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/tree_loading.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/validatebox_pointer.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/images/validatebox_warning.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/layout.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/linkbutton.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/menu.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/menubutton.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/messager.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/pagination.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/panel.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/portal.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/progressbar.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/propertygrid.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/searchbox.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/slider.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/spinner.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/splitbutton.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/tabs.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/tree.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/validatebox.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/themes/gray/window.css create mode 100644 "test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/easyui/\344\277\256\346\224\271\350\257\264\346\230\216.txt" create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icon.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/back.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/blank.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/cancel.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/cut.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/edit_add.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/edit_remove.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/filesave.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/help.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/logo.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/mini_add.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/mini_edit.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/mini_refresh.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/no.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/ok.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/pencil.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/print.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/redo.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/reload.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/search.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/sum.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/tip.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/icons/undo.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/img/glyphicons-halflings-white.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/img/loading.gif create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/img/navbar-bg.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/ksa.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/ksa.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/print/blueprint.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/custom/print/custom.css create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/fusion-charts.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/html5.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/checkbox-check.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/checkbox.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/login-bg.jpg create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/logo-banner.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/report-banner-arrival.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/report-banner.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/type/AE.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/type/AI.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/type/SE.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/images/type/SI.png create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/jquery/jquery-1.7.2.min.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/jquery/jquery.easyui.min.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/jquery/jquery.hotkeys.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/kibo.js create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/rs/readme.txt create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/template/simple/actionerror.ftl create mode 100644 test_input/ksa/ksa-web-root/ksa-web/src/main/webapp/template/simple/actionmessage.ftl create mode 100644 test_input/ksa/ksa-web-root/pom.xml create mode 100644 test_input/ksa/mvn-update-version.sh create mode 100644 test_input/ksa/pom.xml create mode 100644 test_input/whitesource-fs-agent.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17fe345 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +target/ +*.iml +.idea/ +/whitesource-fs-agent.config +/whitesource/ +dots/ +out.txt +out/ +src/test/resources/resolver/npm/sample/node_modules/ +src/test/resources/resolver/npm/sample/package-lock.json \ No newline at end of file diff --git a/.whitesource b/.whitesource new file mode 100644 index 0000000..f340c5d --- /dev/null +++ b/.whitesource @@ -0,0 +1,8 @@ +########################################################## +#### WhiteSource Integration configuration file #### +########################################################## + +# Configuration # +#---------------# +ws.repo.scan=true +vulnerable.check.run.conclusion.level=failure diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.EUA.md b/README.EUA.md new file mode 100644 index 0000000..1051ca4 --- /dev/null +++ b/README.EUA.md @@ -0,0 +1,9 @@ +WhiteSource File System Agent +============================== +## Begin of readme.eua.md + +The WhiteSource File System Agent is distributed together with a modified version +of T.J. Watson Libraries for Analysis http://wala.sourceforge.net. The modified source code is available upon request. +For more details please contact oss@whitesourcesoftware.com + +## End of readme.eua.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a513edb --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +WhiteSource File System Agent +============================== + +**As part of WhiteSource's [Unified Agent](https://whitesource.atlassian.net/wiki/spaces/WD/pages/33718339/Unified+Agent) strategy and roadmap, we have updated the latest versions of the Unified Agent (v18.12.2 and above) with a WhiteSource commercial license. A separate private GitHub repository was created for this purpose and it contains versions 18.12.2 and above of the Unified Agent. The Unified Agent distribution repository can be found [here](https://github.com/whitesource/unified-agent-distribution). For more information on these changes, click [here](https://whitesource.atlassian.net/wiki/spaces/WD/pages/718405635/WhiteSource+Unified+Agent+Updates+January+2019).** + +An [external update agent][1] for projects. + +The agent looks for open source usage in your projects and update your [White Source][2] account. + +### Getting Started +Setup and configuration along with comprehensive documentation could be found [here][3]. +Technical information about the plugin could be found [here][4]. + +### Support +You can always create an issue or tell our support team what you think [here][5]. + +### License +The project is licensed under the [Apache 2.0][6] license. +
+Copyright (C) 2015 WhiteSource Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+ +[1]: http://www.github.com/whitesource/agents +[2]: http://www.whitesourcesoftware.com +[3]: https://whitesource.atlassian.net/wiki/spaces/WD/pages/33718339/File+System+Agent +[4]: https://github.com/whitesource/fs-agent +[5]: mailto:support@whitesourcesoftware.com +[6]: http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/folder-offline-test/whitesource/update-request.txt b/folder-offline-test/whitesource/update-request.txt new file mode 100644 index 0000000..03c2624 --- /dev/null +++ b/folder-offline-test/whitesource/update-request.txt @@ -0,0 +1 @@ +{"updateType":"OVERRIDE","type":"UPDATE","agent":"fs-agent","agentVersion":"2.7.7","pluginVersion":"18.7.2-SNAPSHOT","orgToken":"token","product":"fsAgentMain","productVersion":"","timeStamp":1532939765748,"projects":[{"coordinates":{"artifactId":"npm","version":""},"dependencies":[{"groupId":"request","artifactId":"request-2.83.0.tgz","version":"2.83.0","sha1":"ca0b65da02ed62935887808e6f510381034e3356","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\request\\package.json","optional":false,"children":[{"groupId":"aws-sign2","artifactId":"aws-sign2-0.7.0.tgz","version":"0.7.0","sha1":"b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\aws-sign2\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\aws-sign2\\package.json","dependencyType":"NPM","checksums":{"SHA1":"b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"}},{"groupId":"aws4","artifactId":"aws4-1.7.0.tgz","version":"1.7.0","sha1":"d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\aws4\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\aws4\\package.json","dependencyType":"NPM","checksums":{"SHA1":"d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"}},{"groupId":"caseless","artifactId":"caseless-0.12.0.tgz","version":"0.12.0","sha1":"1b681c21ff84033c826543090689420d187151dc","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\caseless\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\caseless\\package.json","dependencyType":"NPM","checksums":{"SHA1":"1b681c21ff84033c826543090689420d187151dc"}},{"groupId":"combined-stream","artifactId":"combined-stream-1.0.6.tgz","version":"1.0.6","sha1":"723e7df6e801ac5613113a7e445a9b69cb632818","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\combined-stream\\package.json","optional":false,"children":[{"groupId":"delayed-stream","artifactId":"delayed-stream-1.0.0.tgz","version":"1.0.0","sha1":"df3ae199acadfb7d440aaae0b29e2272b24ec619","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\delayed-stream\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\delayed-stream\\package.json","dependencyType":"NPM","checksums":{"SHA1":"df3ae199acadfb7d440aaae0b29e2272b24ec619"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\combined-stream\\package.json","dependencyType":"NPM","checksums":{"SHA1":"723e7df6e801ac5613113a7e445a9b69cb632818"}},{"groupId":"extend","artifactId":"extend-3.0.2.tgz","version":"3.0.2","sha1":"f8b1136b4071fbd8eb140aff858b1019ec2915fa","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\extend\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\extend\\package.json","dependencyType":"NPM","checksums":{"SHA1":"f8b1136b4071fbd8eb140aff858b1019ec2915fa"}},{"groupId":"forever-agent","artifactId":"forever-agent-0.6.1.tgz","version":"0.6.1","sha1":"fbc71f0c41adeb37f96c577ad1ed42d8fdacca91","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\forever-agent\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\forever-agent\\package.json","dependencyType":"NPM","checksums":{"SHA1":"fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"}},{"groupId":"form-data","artifactId":"form-data-2.3.2.tgz","version":"2.3.2","sha1":"4970498be604c20c005d4f5c23aecd21d6b49099","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\form-data\\package.json","optional":false,"children":[{"groupId":"asynckit","artifactId":"asynckit-0.4.0.tgz","version":"0.4.0","sha1":"c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\asynckit\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\asynckit\\package.json","dependencyType":"NPM","checksums":{"SHA1":"c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\form-data\\package.json","dependencyType":"NPM","checksums":{"SHA1":"4970498be604c20c005d4f5c23aecd21d6b49099"}},{"groupId":"har-validator","artifactId":"har-validator-5.0.3.tgz","version":"5.0.3","sha1":"ba402c266194f15956ef15e0fcf242993f6a7dfd","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\har-validator\\package.json","optional":false,"children":[{"groupId":"ajv","artifactId":"ajv-5.5.2.tgz","version":"5.5.2","sha1":"73b5eeca3fab653e3d3f9422b341ad42205dc965","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\ajv\\package.json","optional":false,"children":[{"groupId":"co","artifactId":"co-4.6.0.tgz","version":"4.6.0","sha1":"6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\co\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\co\\package.json","dependencyType":"NPM","checksums":{"SHA1":"6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"}},{"groupId":"fast-deep-equal","artifactId":"fast-deep-equal-1.1.0.tgz","version":"1.1.0","sha1":"c053477817c86b51daa853c81e059b733d023614","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\fast-deep-equal\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\fast-deep-equal\\package.json","dependencyType":"NPM","checksums":{"SHA1":"c053477817c86b51daa853c81e059b733d023614"}},{"groupId":"fast-json-stable-stringify","artifactId":"fast-json-stable-stringify-2.0.0.tgz","version":"2.0.0","sha1":"d5142c0caee6b1189f87d3a76111064f86c8bbf2","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\fast-json-stable-stringify\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\fast-json-stable-stringify\\package.json","dependencyType":"NPM","checksums":{"SHA1":"d5142c0caee6b1189f87d3a76111064f86c8bbf2"}},{"groupId":"json-schema-traverse","artifactId":"json-schema-traverse-0.3.1.tgz","version":"0.3.1","sha1":"349a6d44c53a51de89b40805c5d5e59b417d3340","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\json-schema-traverse\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\json-schema-traverse\\package.json","dependencyType":"NPM","checksums":{"SHA1":"349a6d44c53a51de89b40805c5d5e59b417d3340"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\ajv\\package.json","dependencyType":"NPM","checksums":{"SHA1":"73b5eeca3fab653e3d3f9422b341ad42205dc965"}},{"groupId":"har-schema","artifactId":"har-schema-2.0.0.tgz","version":"2.0.0","sha1":"a94c2224ebcac04782a0d9035521f24735b7ec92","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\har-schema\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\har-schema\\package.json","dependencyType":"NPM","checksums":{"SHA1":"a94c2224ebcac04782a0d9035521f24735b7ec92"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\har-validator\\package.json","dependencyType":"NPM","checksums":{"SHA1":"ba402c266194f15956ef15e0fcf242993f6a7dfd"}},{"groupId":"hawk","artifactId":"hawk-6.0.2.tgz","version":"6.0.2","sha1":"af4d914eb065f9b5ce4d9d11c1cb2126eecc3038","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\hawk\\package.json","optional":false,"children":[{"groupId":"boom","artifactId":"boom-4.3.1.tgz","version":"4.3.1","sha1":"4f8a3005cb4a7e3889f749030fd25b96e01d2e31","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\boom\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\boom\\package.json","dependencyType":"NPM","checksums":{"SHA1":"4f8a3005cb4a7e3889f749030fd25b96e01d2e31"}},{"groupId":"cryptiles","artifactId":"cryptiles-3.1.2.tgz","version":"3.1.2","sha1":"a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\cryptiles\\package.json","optional":false,"children":[{"groupId":"boom","artifactId":"boom-5.2.0.tgz","version":"5.2.0","sha1":"5dd9da6ee3a5f302077436290cb717d3f4a54e02","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\cryptiles\\node_modules\\boom\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\cryptiles\\node_modules\\boom\\package.json","dependencyType":"NPM","checksums":{"SHA1":"5dd9da6ee3a5f302077436290cb717d3f4a54e02"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\cryptiles\\package.json","dependencyType":"NPM","checksums":{"SHA1":"a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"}},{"groupId":"hoek","artifactId":"hoek-4.2.1.tgz","version":"4.2.1","sha1":"9634502aa12c445dd5a7c5734b572bb8738aacbb","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\hoek\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\hoek\\package.json","dependencyType":"NPM","checksums":{"SHA1":"9634502aa12c445dd5a7c5734b572bb8738aacbb"}},{"groupId":"sntp","artifactId":"sntp-2.1.0.tgz","version":"2.1.0","sha1":"2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\sntp\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\sntp\\package.json","dependencyType":"NPM","checksums":{"SHA1":"2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\hawk\\package.json","dependencyType":"NPM","checksums":{"SHA1":"af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"}},{"groupId":"http-signature","artifactId":"http-signature-1.2.0.tgz","version":"1.2.0","sha1":"9aecd925114772f3d95b65a60abb8f7c18fbace1","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\http-signature\\package.json","optional":false,"children":[{"groupId":"assert-plus","artifactId":"assert-plus-1.0.0.tgz","version":"1.0.0","sha1":"f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\assert-plus\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\assert-plus\\package.json","dependencyType":"NPM","checksums":{"SHA1":"f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"}},{"groupId":"jsprim","artifactId":"jsprim-1.4.1.tgz","version":"1.4.1","sha1":"313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\jsprim\\package.json","optional":false,"children":[{"groupId":"extsprintf","artifactId":"extsprintf-1.3.0.tgz","version":"1.3.0","sha1":"96918440e3041a7a414f8c52e3c574eb3c3e1e05","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\extsprintf\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\extsprintf\\package.json","dependencyType":"NPM","checksums":{"SHA1":"96918440e3041a7a414f8c52e3c574eb3c3e1e05"}},{"groupId":"json-schema","artifactId":"json-schema-0.2.3.tgz","version":"0.2.3","sha1":"b480c892e59a2f05954ce727bd3f2a4e882f9e13","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\json-schema\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\json-schema\\package.json","dependencyType":"NPM","checksums":{"SHA1":"b480c892e59a2f05954ce727bd3f2a4e882f9e13"}},{"groupId":"verror","artifactId":"verror-1.10.0.tgz","version":"1.10.0","sha1":"3a105ca17053af55d6e270c1f8288682e18da400","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\verror\\package.json","optional":false,"children":[{"groupId":"core-util-is","artifactId":"core-util-is-1.0.2.tgz","version":"1.0.2","sha1":"b5fd54220aa2bc5ab57aab7140c940754503c1a7","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\core-util-is\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\core-util-is\\package.json","dependencyType":"NPM","checksums":{"SHA1":"b5fd54220aa2bc5ab57aab7140c940754503c1a7"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\verror\\package.json","dependencyType":"NPM","checksums":{"SHA1":"3a105ca17053af55d6e270c1f8288682e18da400"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\jsprim\\package.json","dependencyType":"NPM","checksums":{"SHA1":"313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"}},{"groupId":"sshpk","artifactId":"sshpk-1.14.2.tgz","version":"1.14.2","sha1":"c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\sshpk\\package.json","optional":false,"children":[{"groupId":"asn1","artifactId":"asn1-0.2.3.tgz","version":"0.2.3","sha1":"dac8787713c9966849fc8180777ebe9c1ddf3b86","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\asn1\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\asn1\\package.json","dependencyType":"NPM","checksums":{"SHA1":"dac8787713c9966849fc8180777ebe9c1ddf3b86"}},{"groupId":"bcrypt-pbkdf","artifactId":"bcrypt-pbkdf-1.0.2.tgz","version":"1.0.2","sha1":"a4301d389b6a43f9b67ff3ca11a3f6637e360e9e","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\bcrypt-pbkdf\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\bcrypt-pbkdf\\package.json","dependencyType":"NPM","checksums":{"SHA1":"a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"}},{"groupId":"dashdash","artifactId":"dashdash-1.14.1.tgz","version":"1.14.1","sha1":"853cfa0f7cbe2fed5de20326b8dd581035f6e2f0","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\dashdash\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\dashdash\\package.json","dependencyType":"NPM","checksums":{"SHA1":"853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"}},{"groupId":"ecc-jsbn","artifactId":"ecc-jsbn-0.1.2.tgz","version":"0.1.2","sha1":"3a83a904e54353287874c564b7549386849a98c9","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\ecc-jsbn\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\ecc-jsbn\\package.json","dependencyType":"NPM","checksums":{"SHA1":"3a83a904e54353287874c564b7549386849a98c9"}},{"groupId":"getpass","artifactId":"getpass-0.1.7.tgz","version":"0.1.7","sha1":"5eff8e3e684d569ae4cb2b1282604e8ba62149fa","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\getpass\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\getpass\\package.json","dependencyType":"NPM","checksums":{"SHA1":"5eff8e3e684d569ae4cb2b1282604e8ba62149fa"}},{"groupId":"jsbn","artifactId":"jsbn-0.1.1.tgz","version":"0.1.1","sha1":"a5e654c2e5a2deb5f201d96cefbca80c0ef2f513","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\jsbn\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\jsbn\\package.json","dependencyType":"NPM","checksums":{"SHA1":"a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"}},{"groupId":"safer-buffer","artifactId":"safer-buffer-2.1.2.tgz","version":"2.1.2","sha1":"44fa161b0187b9549dd84bb91802f9bd8385cd6a","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\safer-buffer\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\safer-buffer\\package.json","dependencyType":"NPM","checksums":{"SHA1":"44fa161b0187b9549dd84bb91802f9bd8385cd6a"}},{"groupId":"tweetnacl","artifactId":"tweetnacl-0.14.5.tgz","version":"0.14.5","sha1":"5ae68177f192d4456269d108afa93ff8743f4f64","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\tweetnacl\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\tweetnacl\\package.json","dependencyType":"NPM","checksums":{"SHA1":"5ae68177f192d4456269d108afa93ff8743f4f64"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\sshpk\\package.json","dependencyType":"NPM","checksums":{"SHA1":"c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\http-signature\\package.json","dependencyType":"NPM","checksums":{"SHA1":"9aecd925114772f3d95b65a60abb8f7c18fbace1"}},{"groupId":"is-typedarray","artifactId":"is-typedarray-1.0.0.tgz","version":"1.0.0","sha1":"e479c80858df0c1b11ddda6940f96011fcda4a9a","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\is-typedarray\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\is-typedarray\\package.json","dependencyType":"NPM","checksums":{"SHA1":"e479c80858df0c1b11ddda6940f96011fcda4a9a"}},{"groupId":"isstream","artifactId":"isstream-0.1.2.tgz","version":"0.1.2","sha1":"47e63f7af55afa6f92e1500e690eb8b8529c099a","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\isstream\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\isstream\\package.json","dependencyType":"NPM","checksums":{"SHA1":"47e63f7af55afa6f92e1500e690eb8b8529c099a"}},{"groupId":"json-stringify-safe","artifactId":"json-stringify-safe-5.0.1.tgz","version":"5.0.1","sha1":"1296a2d58fd45f19a0f6ce01d65701e2c735b6eb","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\json-stringify-safe\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\json-stringify-safe\\package.json","dependencyType":"NPM","checksums":{"SHA1":"1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"}},{"groupId":"mime-types","artifactId":"mime-types-2.1.19.tgz","version":"2.1.19","sha1":"71e464537a7ef81c15f2db9d97e913fc0ff606f0","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\mime-types\\package.json","optional":false,"children":[{"groupId":"mime-db","artifactId":"mime-db-1.35.0.tgz","version":"1.35.0","sha1":"0569d657466491283709663ad379a99b90d9ab47","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\mime-db\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\mime-db\\package.json","dependencyType":"NPM","checksums":{"SHA1":"0569d657466491283709663ad379a99b90d9ab47"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\mime-types\\package.json","dependencyType":"NPM","checksums":{"SHA1":"71e464537a7ef81c15f2db9d97e913fc0ff606f0"}},{"groupId":"oauth-sign","artifactId":"oauth-sign-0.8.2.tgz","version":"0.8.2","sha1":"46a6ab7f0aead8deae9ec0565780b7d4efeb9d43","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\oauth-sign\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\oauth-sign\\package.json","dependencyType":"NPM","checksums":{"SHA1":"46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"}},{"groupId":"performance-now","artifactId":"performance-now-2.1.0.tgz","version":"2.1.0","sha1":"6309f4e0e5fa913ec1c69307ae364b4b377c9e7b","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\performance-now\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\performance-now\\package.json","dependencyType":"NPM","checksums":{"SHA1":"6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"}},{"groupId":"qs","artifactId":"qs-6.5.2.tgz","version":"6.5.2","sha1":"cb3ae806e8740444584ef154ce8ee98d403f3e36","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\qs\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\qs\\package.json","dependencyType":"NPM","checksums":{"SHA1":"cb3ae806e8740444584ef154ce8ee98d403f3e36"}},{"groupId":"safe-buffer","artifactId":"safe-buffer-5.1.2.tgz","version":"5.1.2","sha1":"991ec69d296e0313747d59bdfd2b745c35f8828d","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\safe-buffer\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\safe-buffer\\package.json","dependencyType":"NPM","checksums":{"SHA1":"991ec69d296e0313747d59bdfd2b745c35f8828d"}},{"groupId":"stringstream","artifactId":"stringstream-0.0.6.tgz","version":"0.0.6","sha1":"7880225b0d4ad10e30927d167a1d6f2fd3b33a72","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\stringstream\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\stringstream\\package.json","dependencyType":"NPM","checksums":{"SHA1":"7880225b0d4ad10e30927d167a1d6f2fd3b33a72"}},{"groupId":"tough-cookie","artifactId":"tough-cookie-2.3.4.tgz","version":"2.3.4","sha1":"ec60cee38ac675063ffc97a5c18970578ee83655","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\tough-cookie\\package.json","optional":false,"children":[{"groupId":"punycode","artifactId":"punycode-1.4.1.tgz","version":"1.4.1","sha1":"c0d5a63b2718800ad8e1eb0fa5269c84dd41845e","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\punycode\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\punycode\\package.json","dependencyType":"NPM","checksums":{"SHA1":"c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\tough-cookie\\package.json","dependencyType":"NPM","checksums":{"SHA1":"ec60cee38ac675063ffc97a5c18970578ee83655"}},{"groupId":"tunnel-agent","artifactId":"tunnel-agent-0.6.0.tgz","version":"0.6.0","sha1":"27a5dea06b36b04a0a9966774b290868f0fc40fd","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\tunnel-agent\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\tunnel-agent\\package.json","dependencyType":"NPM","checksums":{"SHA1":"27a5dea06b36b04a0a9966774b290868f0fc40fd"}}],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\request\\package.json","dependencyType":"NPM","checksums":{"SHA1":"ca0b65da02ed62935887808e6f510381034e3356"}},{"groupId":"uuid","artifactId":"uuid-3.1.0.tgz","version":"3.1.0","sha1":"3dd3d3e790abc24d7b0d3a034ffababe28ebbc04","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\uuid\\package.json","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample\\node_modules\\uuid\\package.json","dependencyType":"NPM","checksums":{"SHA1":"3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"}},{"artifactId":"sample.js","sha1":"60639b7e53e467dadcbd6bf00a6daaf5738b278e","fullHash":"1660f12ea5e40066471ee989dd3a4b393eb908b4","mostSigBitsHash":"dc25a8c05c41be6d828d0bd404c172ada6b5fd6e","leastSigBitsHash":"7ccfad2d66f476830cccdb173950a64c81006f4d","otherPlatformSha1":"33dbd00f8459b279344daefbf7118f2eea001504","systemPath":"C:\\Gitub-Projects\\fs-agent\\target\\test-classes\\resolver\\npm\\sample.js","optional":false,"children":[],"exclusions":[],"licenses":[],"copyrights":[],"filename":"sample.js","checksums":{"SHA1":"60639b7e53e467dadcbd6bf00a6daaf5738b278e","SHA1_SUPER_HASH":"1660f12ea5e40066471ee989dd3a4b393eb908b4","SHA1_SUPER_HASH_MSB":"dc25a8c05c41be6d828d0bd404c172ada6b5fd6e","SHA1_SUPER_HASH_LSB":"7ccfad2d66f476830cccdb173950a64c81006f4d","SHA1_NO_HEADER":"e489eba188497f066442d3890943d68b5a0ed7f4","SHA1_NO_COMMENTS_SUPER_HASH":"0376304b0fd59234ca5e336dd83d580c7579b299","SHA1_OTHER_PLATFORM":"33dbd00f8459b279344daefbf7118f2eea001504"}}]}],"aggregateModules":false,"preserveModuleStructure":false} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..29e0c31 --- /dev/null +++ b/pom.xml @@ -0,0 +1,537 @@ + + + 4.0.0 + + org.whitesource + whitesource-fs-agent + 1.8 + jar + + White Source File System Agent + File System Agent is a simple java command line tool which extracts descriptive information from your open source libraries + https://github.com/whitesource/fs-agent + + + UTF-8 + 2.9.5 + 1.7.5 + false + + + + WhiteSource Software + http://whitesourcesoftware.com/ + + 2014 + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + scm:git:git@github.com:whitesource/fs-agent.git + scm:git:git@github.com:whitesource/fs-agent.git + git@github.com:whitesource/fs-agent.git + + + https://github.com/whitesource/fs-agent/issues + GitHub Issues + + + + + anna.rozin + Anna Rozin + anna.rozin@whitesourcesoftware.com + + + tom.shapira + Tom Shapira + tom.shapira@whitesourcesoftware.com + + + + + + javax.servlet + javax.servlet-api + 3.1.0 + + + + net.lingala.zip4j + zip4j + 1.3.2 + + + org.apache.httpcomponents + httpclient + 4.3.4 + + + org.zeroturnaround + zt-exec + 1.9 + + + org.jasypt + jasypt + 1.9.2 + + + com.fasterxml.jackson.core + jackson-databind + 2.8.7 + + + javax.mail + mail + 1.5.0-b01 + + + commons-io + commons-io + 2.5 + + + org.apache.commons + commons-lang3 + 3.5 + + + org.apache.logging.log4j + log4j-core + 2.10.0 + + + org.junit.jupiter + junit-jupiter-api + 5.4.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.4.0 + test + + + org.junit.platform + junit-platform-launcher + 1.4.0 + test + + + io.vertx + vertx-core + 3.5.4 + + + + io.vertx + vertx-web + 3.5.4 + + + + + org.apache.maven + maven-model-builder + 3.5.2 + + + org.whitesource + maven-dependency-tree-parser + 1.0.5 + + + + + org.whitesource + wss-agent-api-client + ${agent.api.version} + + + org.whitesource + wss-agent-report + ${agent.api.version} + + + + + com.beust + jcommander + 1.35 + + + + + org.apache.ant + ant + 1.9.4 + + + + + org.eclipse.jgit + org.eclipse.jgit + 4.10.0.201712302008-r + + + + + org.tmatesoft.svnkit + svnkit + 1.8.7 + + + + + com.aragost.javahg + javahg + 0.4 + + + + + net.lingala.zip4j + zip4j + 1.3.2 + + + + + + + org.apache.commons + commons-compress + 1.18 + + + + + com.github.junrar + junrar + 1.0.1 + + + + + org.redline-rpm + redline + 1.2.1 + + + + commons-io + commons-io + 2.4 + + + + + org.json + json + 20170516 + + + + + org.apache.commons + commons-lang3 + 3.4 + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + 1.2.3 + + + org.slf4j + slf4j-api + + + + + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.9.1 + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.9.2 + + + + + org.jsoup + jsoup + 1.11.3 + + + + + + javax.xml.bind + jaxb-api + + 2.3.0-b170201.1204 + + + + + org.simpleframework + simple-xml + 2.7.1 + + + + + junit + junit + 4.12 + test + + + + io.vertx + vertx-unit + 3.0.0 + test + + + + org.codehaus.plexus + plexus-archiver + 3.4 + + + org.codehaus.plexus + plexus-container-default + 1.7.1 + + + + + com.amazonaws + aws-java-sdk-ecr + 1.11.410 + + + com.sun.jersey + jersey-client + 1.17.1 + + + + + + + src/main/resources + true + + + + . + + README.md + README.EUA.md + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + maven-surefire-plugin + 2.20.1 + + + default-test + test + + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + + + + maven-assembly-plugin + 2.4 + + + + org.whitesource.fs.Main + + + + jar-with-dependencies + + whitesource-fs-agent-${project.version} + false + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8.1 + + + org.apache.maven.plugins + maven-gpg-plugin + 1.4 + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + -Dgpg.passphrase=${passed.gpg.passphrase} -Dgpg.homedir=${passed.gpg.homedir} + Release + + + + + + + org.whitesource + whitesource-maven-plugin + 3.2.5 + + ${whitesource.orgToken} + File System Agent + + + + + + + + + ci-build + + false + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + ${gpg.homedir} + + + + + + + + + Release + + false + + + + + com.github.github + site-maven-plugin + + + + site + + site + + + + + + + + + + + github + GitHub OWNER Apache Maven Packages + https://maven.pkg.github.com/whitesource + + + diff --git a/shiftleft.json b/shiftleft.json new file mode 100644 index 0000000..7771ef3 --- /dev/null +++ b/shiftleft.json @@ -0,0 +1,5 @@ +{ + "license": "ZXlKaGJHY2lPaUpTVXpVeE1pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBZWFFpT2pFMU56UXdOalUxTURNc0ltbHpjeUk2SWxOb2FXWjBUR1ZtZENJc0ltOXlaMGxFSWpvaVlUTmtNemcxTVRJdFlqTXhPUzAwTXpZd0xXSmtZVFF0TUdZeVlXWTNOek01TmpjNElpd2lkWE5sY2tsRUlqb2lORFJsWlRKak5HRXRNbVprWXkwMFlqSmhMV0k1TUdZdE0ySmpNV1ExWkdObVptSTFJaXdpYzJOdmNHVnpJanBiSW5ObFlYUnpPbmR5YVhSbElpd2laWGgwWlc1a1pXUWlMQ0poY0drNmRqSWlMQ0oxY0d4dllXUnpPbmR5YVhSbElpd2liRzluT25keWFYUmxJaXdpY0dsd1pXeHBibVZ6ZEdGMGRYTTZjbVZoWkNJc0ltMWxkSEpwWTNNNmQzSnBkR1VpTENKd2IyeHBZMmxsY3pwamRYTjBiMjFsY2lKZGZRLnloeURiVVdkVmM1VmdMbi1yUUVWNWtBalVxejZHWHM5ZkNJVlh1VXVMNVh0ZnNlR3BoMFZsMTQxOFRWdW9VWkU0b2dxejVkbGItcGlNYVdhcHpPQ1dBdjAyejZMNktmRFg2ZThmMjNqS3Vrc2FiVy03WE5SVF91QjN1bTdvUzdwY29NUHN5MVhUcXVPazJlNDVLOTB0SnljSmxzR28yNmUyVmRFczdLWE9oZkltWjFvNDhkVldCOEZFUVM3QkRvZ1B2MkhndTVlSnpSOVhIOG9DRXFYcTB0dm9fblExVXZnWkUtX1Mybk41WHVpTm1qYXdaVWdQZlp1WFhWcnNyNUR0VmtRMWpEYXBDRUZ5dkxQUGdRS1FmTHJzdGJJcXlNSkQwZzg0X3hmc25fY3hRZTIya0VBeXB4WGRQSEtTcmNmbWxrdnF4MlNZdDZDSGdvcS04bGJWdw==", + "sprId": "sl/a3d38512-b319-4360-bda4-0f2af7739678/fs-agent/5b4e048c983b1bc4c687b333000074ccbe1d464f", + "accessToken": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1NzQwNjU1MDMsImlzcyI6IlNoaWZ0TGVmdCIsIm9yZ0lEIjoiYTNkMzg1MTItYjMxOS00MzYwLWJkYTQtMGYyYWY3NzM5Njc4IiwidXNlcklEIjoiNDRlZTJjNGEtMmZkYy00YjJhLWI5MGYtM2JjMWQ1ZGNmZmI1Iiwic2NvcGVzIjpbInNlYXRzOndyaXRlIiwiZXh0ZW5kZWQiLCJhcGk6djIiLCJ1cGxvYWRzOndyaXRlIiwibG9nOndyaXRlIiwicGlwZWxpbmVzdGF0dXM6cmVhZCIsIm1ldHJpY3M6d3JpdGUiLCJwb2xpY2llczpjdXN0b21lciJdfQ.yhyDbUWdVc5VgLn-rQEV5kAjUqz6GXs9fCIVXuUuL5XtfseGph0Vl1418TVuoUZE4ogqz5dlb-piMaWapzOCWAv02z6L6KfDX6e8f23jKuksabW-7XNRT_uB3um7oS7pcoMPsy1XTquOk2e45K90tJycJlsGo26e2VdEs7KXOhfImZ1o48dVWB8FEQS7BDogPv2Hgu5eJzR9XH8oCEqXq0tvo_nQ1UvgZE-_S2nN5XuiNmjawZUgPfZuXXVrsr5DtVkQ1jDapCEFyvLPPgQKQfLrstbIqyMJD0g84_xfsn_cxQe22kEAypxXdPHKSrcfmlkvqx2SYt6CHgoq-8lbVw" +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/ConfigPropertyKeys.java b/src/main/java/org/whitesource/agent/ConfigPropertyKeys.java new file mode 100644 index 0000000..e1ddb79 --- /dev/null +++ b/src/main/java/org/whitesource/agent/ConfigPropertyKeys.java @@ -0,0 +1,252 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent; + +/** + * Property keys for the whitesource-docker-agent.configuration file. + * + * @author itai.marko + * @author tom.shapira + */ +public final class ConfigPropertyKeys { + + public static final String CHECK_POLICIES_PROPERTY_KEY = "checkPolicies"; + public static final String FORCE_UPDATE = "forceUpdate"; + public static final String FORCE_UPDATE_FAIL_BUILD_ON_POLICY_VIOLATION = "forceUpdate.failBuildOnPolicyViolation"; + public static final String FORCE_CHECK_ALL_DEPENDENCIES = "forceCheckAllDependencies"; // optional + public static final String UPDATE_INVENTORY = "updateInventory"; // optional + public static final String ENABLE_IMPACT_ANALYSIS = "enableImpactAnalysis"; // optional + public static final String IA_LANGUAGE = "iaLanguage"; // optional + public static final String CONNECTION_RETRIES = "connectionRetries"; + public static final String CONNECTION_RETRIES_INTERVALS = "connectionRetriesInterval"; + public static final String ORG_TOKEN_PROPERTY_KEY = "apiKey"; + public static final String ORG_TOKEN_FILE = "apiKeyFile"; + public static final String USER_KEY_PROPERTY_KEY = "userKey"; + public static final String USER_KEY_FILE = "userKeyFile"; + public static final String PARTIAL_SHA1_MATCH_KEY = "partialSha1Match"; + public static final String PRODUCT_TOKEN_PROPERTY_KEY = "productToken"; // optional + public static final String PRODUCT_NAME_PROPERTY_KEY = "productName"; // optional + public static final String PRODUCT_VERSION_PROPERTY_KEY = "productVersion"; // optional + public static final String APP_PATH = "appPath"; // optional + public static final String X_PATHS = "xPaths"; // optional + public static final String VIA_DEBUG = "viaDebug"; // optional + public static final String VIA_ANALYSIS_LEVEL = "viaAnalysysLevel"; // optional + public static final String PROJECT_TOKEN_PROPERTY_KEY = "projectToken"; + public static final String PROJECT_NAME_PROPERTY_KEY = "projectName"; + public static final String PROJECT_VERSION_PROPERTY_KEY = "projectVersion"; // optional + public static final String INCLUDES_PATTERN_PROPERTY_KEY = "includes"; + public static final String EXCLUDES_PATTERN_PROPERTY_KEY = "excludes"; + public static final String ARCHIVE_EXTRACTION_DEPTH_KEY = "archiveExtractionDepth"; + public static final String ARCHIVE_INCLUDES_PATTERN_KEY = "archiveIncludes"; + public static final String ARCHIVE_EXCLUDES_PATTERN_KEY = "archiveExcludes"; + public static final String ARCHIVE_FAST_UNPACK_KEY = "archiveFastUnpack"; + public static final String CALCULATE_HINTS = "calculate.hints"; + public static final String CALCULATE_MD5 = "calculate.md5"; + public static final String REQUESTER_EMAIL = "requesterEmail"; + public static final String CASE_SENSITIVE_GLOB_PROPERTY_KEY = "case.sensitive.glob"; + public static final String PROXY_HOST_PROPERTY_KEY = "proxy.host"; + public static final String PROXY_PORT_PROPERTY_KEY = "proxy.port"; + public static final String PROXY_USER_PROPERTY_KEY = "proxy.user"; + public static final String PROXY_PASS_PROPERTY_KEY = "proxy.pass"; + public static final String IGNORE_CERTIFICATE_CHECK = "ignoreCertificateCheck"; + public static final String OFFLINE_PROPERTY_KEY = "offline"; + public static final String OFFLINE_ZIP_PROPERTY_KEY = "offline.zip"; + public static final String OFFLINE_PRETTY_JSON_KEY = "offline.prettyJson"; + public static final String SCM_TYPE_PROPERTY_KEY = "scm.type"; + public static final String SCM_URL_PROPERTY_KEY = "scm.url"; + public static final String SCM_PPK_PROPERTY_KEY = "scm.ppk"; + public static final String SCM_USER_PROPERTY_KEY = "scm.user"; + public static final String SCM_PASS_PROPERTY_KEY = "scm.pass"; + public static final String SCM_BRANCH_PROPERTY_KEY = "scm.branch"; + public static final String SCM_TAG_PROPERTY_KEY = "scm.tag"; + public static final String SCM_NPM_INSTALL = "scm.npmInstall"; + public static final String SCM_NPM_INSTALL_TIMEOUT_MINUTES = "scm.npmInstallTimeoutMinutes"; + public static final String SCM_REPOSITORIES_FILE = "scm.repositoriesFile"; + public static final String EXCLUDED_COPYRIGHT_KEY = "copyright.excludes"; + public static final String LOG_LEVEL_KEY = "log.level"; + public static final String FOLLOW_SYMBOLIC_LINKS = "followSymbolicLinks"; + public static final String SHOW_PROGRESS_BAR = "showProgressBar"; + public static final String ACCEPT_EXTENSIONS_LIST = "acceptExtensionsList"; + + public static final String RESOLVE_ALL_DEPENDENCIES = "resolveAllDependencies"; + + public static final String NPM_RUN_PRE_STEP = "npm.runPreStep"; + public static final String NPM_IGNORE_SCRIPTS = "npm.ignoreScripts"; + public static final String NPM_RESOLVE_DEPENDENCIES = "npm.resolveDependencies"; + public static final String NPM_INCLUDE_DEV_DEPENDENCIES = "npm.includeDevDependencies"; + public static final String NPM_TIMEOUT_DEPENDENCIES_COLLECTOR_SECONDS = "npm.timeoutDependenciesCollectorInSeconds"; + public static final String NPM_ACCESS_TOKEN = "npm.accessToken"; + public static final String NPM_IGNORE_NPM_LS_ERRORS = "npm.ignoreNpmLsErrors"; + public static final String NPM_YARN_PROJECT = "npm.yarnProject"; + public static final String NPM_IGNORE_JAVA_SCRIPT_FILES = "npm.ignoreJavaScriptFiles"; + public static final String NPM_IGNORE_SOURCE_FILES = "npm.ignoreSourceFiles"; + + + public static final String BOWER_RESOLVE_DEPENDENCIES = "bower.resolveDependencies"; + public static final String BOWER_RUN_PRE_STEP = "bower.runPreStep"; + public static final String BOWER_IGNORE_SOURCE_FILES = "bower.ignoreSourceFiles"; + + public static final String PYTHON_RESOLVE_DEPENDENCIES = "python.resolveDependencies"; + public static final String PYTHON_PIP_PATH = "python.pipPath"; + public static final String PYTHON_PATH = "python.path"; + public static final String PYTHON_IS_WSS_PLUGIN_INSTALLED = "python.isWssPluginInstalled"; + public static final String PYTHON_UNINSTALL_WSS_PLUGIN = "python.uninstallWssPlugin"; + public static final String PYTHON_IGNORE_PIP_INSTALL_ERRORS = "python.ignorePipInstallErrors"; + public static final String PYTHON_INSTALL_VIRTUALENV = "python.installVirtualenv"; + public static final String PYTHON_RESOLVE_HIERARCHY_TREE = "python.resolveHierarchyTree"; + public static final String PYTHON_RESOLVE_SETUP_PY_FILES = "python.resolveSetupPyFiles"; + public static final String PYTHON_IGNORE_SOURCE_FILES = "python.ignoreSourceFiles"; + public static final String PYTHON_REQUIREMENTS_FILE_INCLUDES = "python.requirementsFileIncludes"; + public static final String PYTHON_RUN_PIPENV_PRE_STEP = "python.runPipenvPreStep"; + public static final String PYTHON_IGNORE_PIPENV_INSTALL_ERRORS = "python.IgnorePipenvInstallErrors"; + public static final String PYTHON_PIPENV_DEV_DEPENDENCIES = "python.pipenvDevDependencies"; + + public static final String NUGET_RESOLVE_DEPENDENCIES = "nuget.resolveDependencies"; + public static final String NUGET_RESTORE_DEPENDENCIES = "nuget.restoreDependencies"; + public static final String NUGET_RUN_PRE_STEP = "nuget.runPreStep"; + public static final String NUGET_IGNORE_SOURCE_FILES = "nuget.ignoreSourceFiles"; + public static final String NUGET_RESOLVE_CS_PROJ_FILES = "nuget.resolveCsProjFiles"; + public static final String NUGET_RESOLVE_PACKAGES_CONFIG_FILES = "nuget.resolvePackagesConfigFiles"; + + public static final String MAVEN_IGNORED_SCOPES = "maven.ignoredScopes"; + public static final String MAVEN_RESOLVE_DEPENDENCIES = "maven.resolveDependencies"; + public static final String MAVEN_AGGREGATE_MODULES = "maven.aggregateModules"; + public static final String MAVEN_IGNORE_POM_MODULES = "maven.ignorePomModules"; + public static final String MAVEN_IGNORE_SOURCE_FILES = "maven.ignoreSourceFiles"; + public static final String MAVEN_RUN_PRE_STEP = "maven.runPreStep"; + public static final String MAVEN_IGNORE_DEPENDENCY_TREE_ERRORS = "maven.ignoreMvnTreeErrors"; + + public static final String IGNORE_SOURCE_FILES = "ignoreSourceFiles"; + + public static final String PROJECT_PER_SUBFOLDER = "projectPerFolder"; + public static final String PROJECT_PER_FOLDER_INCLUDES = "projectPerFolderIncludes"; + public static final String PROJECT_PER_FOLDER_EXCLUDES = "projectPerFolderExcludes"; + public static final String UPDATE_TYPE = "updateType"; + public static final String PROJECT_CONFIGURATION_PATH = "configFilePath"; + public static final String SCAN_PACKAGE_MANAGER = "scanPackageManager"; + public static final String WHITESOURCE_FOLDER_PATH = "whiteSourceFolderPath"; + + public static final String ENDPOINT_ENABLED = "endpoint.enabled"; + public static final String ENDPOINT_PORT = "endpoint.port"; + public static final String ENDPOINT_CERTIFICATE = "endpoint.certificate"; + public static final String ENDPOINT_PASS = "endpoint.pass"; + public static final String ENDPOINT_SSL_ENABLED = "endpoint.ssl"; + + public static final String GRADLE_RUN_PRE_STEP = "gradle.runPreStep"; + public static final String GRADLE_RESOLVE_DEPENDENCIES = "gradle.resolveDependencies"; + public static final String GRADLE_RUN_ASSEMBLE_COMMAND = "gradle.runAssembleCommand"; + public static final String GRADLE_AGGREGATE_MODULES = "gradle.aggregateModules"; + public static final String GRADLE_PREFERRED_ENVIRONMENT = "gradle.preferredEnvironment"; + public static final String GRADLE_IGNORE_SOURCE_FILES = "gradle.ignoreSourceFiles"; + public static final String GRADLE_IGNORE_SCOPES = "gradle.ignoredScopes"; + public static final String GRADLE_LOCAL_REPOSITORY_PATH = "gradle.localRepositoryPath"; + + public static final String PAKET_RESOLVE_DEPENDENCIES = "paket.resolveDependencies"; + public static final String PAKET_IGNORED_GROUPS = "paket.ignoredGroups"; + public static final String PAKET_RUN_PRE_STEP = "paket.runPreStep"; + public static final String PAKET_EXE_PATH = "paket.exePath"; + public static final String PAKET_IGNORE_FILES = "paket.ignoreFiles"; + public static final String PAKET_IGNORE_SOURCE_FILES = "paket.ignoreSourceFiles"; + + public static final String GO_RESOLVE_DEPENDENCIES = "go.resolveDependencies"; + public static final String GO_DEPENDENCY_MANAGER = "go.dependencyManager"; + public static final String GO_COLLECT_DEPENDENCIES_AT_RUNTIME = "go.collectDependenciesAtRuntime"; + public static final String GO_GLIDE_IGNORE_TEST_PACKAGES = "go.glide.ignoreTestPackages"; + public static final String GO_IGNORE_SOURCE_FILES = "go.ignoreSourceFiles"; + public static final String GO_GRADLE_ENABLE_TASK_ALIAS = "go.gogradle.enableTaskAlias"; + + public static final String RUBY_RESOLVE_DEPENDENCIES = "ruby.resolveDependencies"; + public static final String RUBY_RUN_BUNDLE_INSTALL = "ruby.runBundleInstall"; + public static final String RUBY_OVERWRITE_GEM_FILE = "ruby.overwriteGemFile"; + public static final String RUBY_INSTALL_MISSING_GEMS = "ruby.installMissingGems"; + public static final String RUBY_IGNORE_SOURCE_FILES = "ruby.ignoreSourceFiles"; + + public static final String PHP_RESOLVE_DEPENDENCIES = "php.resolveDependencies"; + public static final String PHP_RUN_PRE_STEP = "php.runPreStep"; + public static final String PHP_INCLUDE_DEV_DEPENDENCIES = "php.includeDevDependencies"; + + public static final String SBT_RESOLVE_DEPENDENCIES = "sbt.resolveDependencies"; + public static final String SBT_AGGREGATE_MODULES = "sbt.aggregateModules"; + public static final String SBT_RUN_PRE_STEP = "sbt.runPreStep"; + public static final String SBT_TARGET_FOLDER = "sbt.targetFolder"; + public static final String SBT_IGNORE_SOURCE_FILES = "sbt.ignoreSourceFiles"; + + public static final String HTML_RESOLVE_DEPENDENCIES = "html.resolveDependencies"; + + public static final String COCOAPODS_RESOLVE_DEPENDENCIES = "cocoapods.resolveDependencies"; + public static final String COCOAPODS_RUN_PRE_STEP = "cocoapods.runPreStep"; + public static final String COCOAPODS_IGNORE_SOURCE_FILES = "cocoapods.ignoreSourceFiles"; + + public static final String HEX_RESOLVE_DEPENDENECIES = "hex.resolveDependencies"; + public static final String HEX_RUN_PRE_STEP = "hex.runPreStep"; + public static final String HEX_IGNORE_SOURCE_FILES = "hex.ignoreSourceFiles"; + public static final String HEX_AGGREGATE_MODULES = "hex.aggregateModules"; + + public static final String DEPENDENCIES_ONLY = "dependenciesOnly"; + public static final String WHITESOURCE_CONFIGURATION = "whitesourceConfiguration"; + + public static final String SCANNED_FOLDERS = "d"; + public static final String SEND_LOGS_TO_WSS = "sendLogsToWss"; + + public static final String SCAN_COMMENT = "scanComment"; + + public static final String LOG_CONTEXT = "logContext"; + + public static final String REQUIRE_KNOWN_SHA1 = "requireKnownSha1"; + + // Analysis multi module project for via + public static final String ANALYZE_MULTI_MODULE = "analyzeMultiModule"; + public static final String X_MODULE_PATH = "xModulePath"; + + // Global values for remote Docker + public static final String DOCKER_INCLUDES_PATTERN_PROPERTY_KEY = "docker.includes"; + public static final String DOCKER_EXCLUDES_PATTERN_PROPERTY_KEY = "docker.excludes"; + public static final String SCAN_DOCKER_IMAGES = "docker.scanImages"; + public static final String SCAN_TAR_IMAGES = "docker.tarImages"; + public static final String DELETE_TAR_FILES = "docker.deleteTar"; + public static final String DOCKER_PULL_ENABLE = "docker.pull.enable"; + public static final String DOCKER_PULL_IMAGES = "docker.pull.images"; + public static final String DOCKER_PULL_TAGS = "docker.pull.tags"; + public static final String DOCKER_PULL_DIGEST = "docker.pull.digest"; + public static final String DOCKER_PULL_MAX_IMAGES = "docker.pull.maxImages"; + public static final String DOCKER_DELETE_FORCE = "docker.delete.force"; + public static final String DOCKER_LOGIN_SUDO = "docker.login.sudo"; + // TODO: Not implemented yet + public static final String DOCKER_SCAN_MAX_IMAGES = "docker.scan.maxImages"; + // TODO: Not implemented yet + public static final String DOCKER_PULL_FORCE = "docker.pull.force"; + + // Values for remote Docker on AWS ECR + public static final String DOCKER_AWS_ENABLE = "docker.aws.enable"; + public static final String DOCKER_AWS_REGISTRY_IDS = "docker.aws.registryIds"; + public static final String DOCKER_AWS_REGION = "docker.aws.region"; + // TODO: Not implemented yet + public static final String DOCKER_AWS_MAX_PULL_IMAGES = "docker.aws.maxPullImages"; + // TODO: Not implemented yet + public static final String DOCKER_AWS_ACCESSKEY = "docker.aws.accessKey"; + // TODO: Not implemented yet + public static final String DOCKER_AWS_SECRETKEY = "docker.aws.secretKey"; + + // Remote Docker Azure + public static final String DOCKER_AZURE_ENABLED = "docker.azure.enable"; + public static final String DOCKER_AZURE_USER_NAME = "docker.azure.userName"; + public static final String DOCKER_AZURE_USER_PASSWORD = "docker.azure.userPassword"; + public static final String DOCKER_AZURE_REGISTRY_NAMES = "docker.azure.registryNames"; + + + public static final String ADD_SHA1 = "addSha1"; + public static final String NO_CONFIG = "noConfig"; +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/Constants.java b/src/main/java/org/whitesource/agent/Constants.java new file mode 100644 index 0000000..1ac7910 --- /dev/null +++ b/src/main/java/org/whitesource/agent/Constants.java @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent; + +/** + * Property keys for the whitesource-docker-agent.configuration file. + * + * @author annarozin + */ +public final class Constants { + + public static final String NEW_LINE = System.lineSeparator(); + public static final String JAVA_NETWORKING = "java.net"; + public static final String FILE_SEPARATOR = "file.separator"; + public static final String FALSE = "false"; + public static final String TRUE = "true"; + public static final String TAG = "tag"; + public static final String VERSION = "version"; + public static final String RESOLUTION = "_resolution"; + public static final String NAME = "name"; + public static final String MISSING = "missing"; + public static final String DEPENDENCIES = "dependencies"; + public static final String SRC = "src"; + public static final String CMD = "cmd"; + public static final String OS_NAME = "os.name"; + public static final String WIN = "win"; + public static final String JS_EXTENSION = ".js"; + public static final String PACKAGES = "packages"; + public static final String INSTALL = "install"; + public static final String MAVEN = "maven"; + public static final String HTML = "html"; + public static final String HTM = "htm"; + public static final String SHTML = "shtml"; + public static final String XHTML = "xhtml"; + public static final String JSP = "jsp"; + public static final String ASP = "asp"; + public static final String DO = "do"; + public static final String ASPX = "aspx"; + public static final String WINDOWS = "Windows"; + public static final String GRADLE_WRAPPER = "wrapper"; + public static final String GRADLE = "gradle"; + public static final String POM = "pom"; + public static final String JAR = "jar"; + public static final String DOT = "."; + public static final String DIRECTORY = "d"; + public static final String BACK_SLASH = "\\"; + public static final String FORWARD_SLASH = "/"; + public static final String WHITESPACE = " "; + public static final String EMPTY_STRING = ""; + public static final String COLON = ":"; + public static final String AT = "@"; + public static final String PLUS = "+"; + public static final String DASH = "-"; + public static final String PATTERN = "**/*"; + public static final String COMMA = ","; + public static final String PIPE = "|"; + public static final String REGEX_PATTERN_PREFIX = ".*\\."; + public static final String GLOB_PATTERN_PREFIX = "**/*"; + public static final String GLOB_PATTERN = ".*.*"; + public static final String EQUALS = "="; + public static final String POUND = "#"; + public static final String QUOTATION_MARK = "\""; + public static final String APOSTROPHE = "'"; + public static final String HTTP = "http"; + public static final String HTTPS = "https"; + public static final String UTF8 = "UTF-8"; + public static final String DLL = ".dll"; + public static final String EXE = ".exe"; + public static final String NUPKG = ".nupkg"; + public static final String CS = ".cs"; + public static final String VAR = "var"; + public static final String LIB = "lib"; + public static final String YUM_DB = "yumdb"; + public static final String YUM = "yum"; + public static final String PYTHON_REQUIREMENTS = "requirements.txt"; + public static final String PIPFILE = "Pipfile"; + public static final String TXT_EXTENSION = ".txt"; + public static final String SETUP_PY = "setup.py"; + public static final String JAR_EXTENSION = ".jar"; + public static final int MAX_EXTRACTION_DEPTH = 7; + public static final int COMMENT_MAX_LENGTH = 1000; + public static final int ZERO = 0; + public static final int ONE = 1; + public static final String BUILD_GRADLE = "build.gradle"; + public static final String COPY_DEPENDENCIES = "copyDependencies"; + public static final String UNDERSCORE = "_"; + public static final char QUESTION_MARK = '?'; + public static final char WHITESPACE_CHAR = ' '; + public static final char OPEN_BRACKET = '('; + public static final char CLOSE_BRACKET = ')'; + public static final char EQUALS_CHAR = '='; + public static final char OPEN_SQUARE_BRACKET = '['; + public static final char CLOSE_SQUARE_BRACKET = ']'; + public static final String DOUBLE_EQUALS = "=="; + public static final char SEMI_COLON = ';'; + public static final String DOLLAR = "$"; + public static final String OPEN_CURLY_BRACKET = "{"; + public static final String CLOSE_CURLY_BRACKET = "}"; + + public static final int MAX_NUMBER_OF_DEPENDENCIES = 1000000; + + public static final String MAP_LOG_NAME = "org.whitesource"; + public static final String MAP_APPENDER_NAME = "collectToMap"; + public static final String HELP_ARG1 = "-help"; + public static final String HELP_ARG2 = "-h"; + public static final String TARGET = "target"; + public static final String BUILD = "build"; + public static final String NONE = "None"; + public static final String LIBS = "libs"; + public static final String USER_HOME = "user.home"; +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/DependencyCalculator.java b/src/main/java/org/whitesource/agent/DependencyCalculator.java new file mode 100644 index 0000000..310242b --- /dev/null +++ b/src/main/java/org/whitesource/agent/DependencyCalculator.java @@ -0,0 +1,79 @@ +package org.whitesource.agent; + +import org.whitesource.agent.api.model.DependencyInfo; + +import java.io.File; +import java.text.MessageFormat; +import java.util.*; + +public class DependencyCalculator { + + private static final List progressAnimation = Arrays.asList("|", "/", Constants.DASH, "\\"); + private static final int ANIMATION_FRAMES = progressAnimation.size(); + private final boolean showProgressBar; + private int animationIndex = 0; + + public DependencyCalculator(boolean showProgressBar) { + this.showProgressBar = showProgressBar; + this.animationIndex = 0; + } + + public Collection createDependencies(boolean scmConnector, int totalFiles, Map> fileMap, + Collection excludedCopyrights, boolean partialSha1Match) { + return createDependencies(scmConnector, totalFiles, fileMap, excludedCopyrights, partialSha1Match, false, false); + } + + public Collection createDependencies(boolean scmConnector, int totalFiles, Map> fileMap, + Collection excludedCopyrights, boolean partialSha1Match, boolean calculateHints, boolean calculateMd5) { + List allDependencies = new ArrayList<>(); + if (showProgressBar) { + displayProgress(0, totalFiles); + } + + int index = 1; + for (Map.Entry> entry : fileMap.entrySet()) { + for (String fileName : entry.getValue()) { + DependencyInfoFactory factory = new DependencyInfoFactory(excludedCopyrights, partialSha1Match, calculateHints, calculateMd5); + DependencyInfo originalDependencyInfo = factory.createDependencyInfo(entry.getKey(), fileName); + if (originalDependencyInfo != null) { + if (scmConnector) { + originalDependencyInfo.setSystemPath(fileName.replace(Constants.BACK_SLASH, Constants.FORWARD_SLASH)); + } + allDependencies.add(originalDependencyInfo); + } + if (showProgressBar) { + displayProgress(index, totalFiles); + } + index++; + } + } + return allDependencies; + } + + private void displayProgress(int index, int totalFiles) { + StringBuilder sb = new StringBuilder("[INFO] "); + + // draw each animation for 4 frames + int actualAnimationIndex = animationIndex % (ANIMATION_FRAMES * 4); + sb.append(progressAnimation.get((actualAnimationIndex / 4) % ANIMATION_FRAMES)); + animationIndex++; + + // draw progress bar + sb.append(" ["); + double percentage = ((double) index / totalFiles) * 100; + int progressionBlocks = (int) (percentage / 3); + for (int i = 0; i < progressionBlocks; i++) { + sb.append(Constants.POUND); + } + for (int i = progressionBlocks; i < 33; i++) { + sb.append(Constants.WHITESPACE); + } + sb.append("] {0}% - {1} of {2} files\r"); + System.out.print(MessageFormat.format(sb.toString(), (int) percentage, index, totalFiles)); + + if (index == totalFiles) { + // clear progress animation + System.out.print(" \r"); + } + } +} diff --git a/src/main/java/org/whitesource/agent/DependencyInfoFactory.java b/src/main/java/org/whitesource/agent/DependencyInfoFactory.java new file mode 100644 index 0000000..b44c4da --- /dev/null +++ b/src/main/java/org/whitesource/agent/DependencyInfoFactory.java @@ -0,0 +1,424 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whitesource.agent.api.model.ChecksumType; +import org.whitesource.agent.api.model.CopyrightInfo; +import org.whitesource.agent.api.model.DependencyHintsInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.hash.ChecksumUtils; +import org.whitesource.agent.hash.HashAlgorithm; +import org.whitesource.agent.hash.HashCalculator; +import org.whitesource.agent.hash.HintUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.*; + +/** + * Factory class for {@link org.whitesource.agent.api.model.DependencyInfo}. + * + * @author tom.shapira + */ +public class DependencyInfoFactory { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(DependencyInfoFactory.class); + + private static final String COPYRIGHT = "copyright"; + private static final String COPYRIGHT_SYMBOL = "(c)"; + private static final String COPYRIGHT_ASCII_SYMBOL = "©"; + private static final List COPYRIGHT_TEXTS = Arrays.asList("copyright (c)", "copyright(c)", "(c) copyright", + "(c)copyright", COPYRIGHT, COPYRIGHT_SYMBOL, COPYRIGHT_ASCII_SYMBOL); + private static final String ALL_RIGHTS_RESERVED = "all rights reserved"; + + private static final String COPYRIGHT_ALPHA_CHAR_REGEX = ".*[a-zA-Z]+.*"; + private static final String CONTAINS_YEAR_REGEX = ".*(\\d\\d\\d\\d)+.*"; + + private static final String JAVA_SCRIPT_REGEX = ".*\\.js"; + + private static final List MATH_SYMBOLS = Arrays.asList('+', '-', '=', '<', '>', '*', '/', '%', '^'); + private static final int MAX_VALID_CHAR_VALUE = 127; + private static final int MAX_INVALID_CHARS = 2; + private static final String DEFINE = "define"; + private static final String TODO_PATTERN = "todo:.*|todo .*"; + private static final String CODE_LINE_SUFFIX = ".*:|.*;|.*\\{|.*}|.*\\[|.*]|.*>"; + + private static final Map commentStartEndMap; + + static { + commentStartEndMap = new HashMap<>(); + commentStartEndMap.put("/*", "*/"); + commentStartEndMap.put("/**", "*/"); + commentStartEndMap.put(""); + commentStartEndMap.put("\"\"\"", "\"\"\""); + commentStartEndMap.put("=begin", "=end"); + commentStartEndMap.put("##", "##"); + } + + /* --- Members --- */ + + private final Collection excludedCopyrights; + private final boolean partialSha1Match; + private boolean calculateHints; + private boolean calculateMd5; + + /* --- Constructors --- */ + + public DependencyInfoFactory() { + excludedCopyrights = new ArrayList<>(); + partialSha1Match = false; + } + + public DependencyInfoFactory(Collection excludedCopyrights, boolean partialSha1Match) { + this.excludedCopyrights = excludedCopyrights; + this.partialSha1Match = partialSha1Match; + } + + public DependencyInfoFactory(Collection excludedCopyrights, boolean partialSha1Match, boolean calculateHints, boolean calculateMd5) { + this(excludedCopyrights, partialSha1Match); + this.calculateHints = calculateHints; + this.calculateMd5 = calculateMd5; + } + + /* --- Public methods --- */ + + public DependencyInfo createDependencyInfo(File basedir, String filename) { + DependencyInfo dependency; + try { + File dependencyFile = new File(basedir, filename); + String sha1 = ChecksumUtils.calculateSHA1(dependencyFile); + dependency = new DependencyInfo(sha1); + dependency.setArtifactId(dependencyFile.getName()); + dependency.setFilename(dependencyFile.getName()); + + // system path + try { + dependency.setSystemPath(dependencyFile.getCanonicalPath()); + } catch (IOException e) { + dependency.setSystemPath(dependencyFile.getAbsolutePath()); + } + + // populate hints + if (calculateHints) { + DependencyHintsInfo hints = HintUtils.getHints(dependencyFile.getPath()); + dependency.setHints(hints); + } + + // additional sha1s + // MD5 + if (calculateMd5) { + String md5 = ChecksumUtils.calculateHash(dependencyFile, HashAlgorithm.MD5); + dependency.addChecksum(ChecksumType.MD5, md5); + } + + // handle JavaScript files + if (filename.toLowerCase().matches(JAVA_SCRIPT_REGEX)) { + Map javaScriptChecksums; + try { + javaScriptChecksums = new HashCalculator().calculateJavaScriptHashes(dependencyFile); + if (javaScriptChecksums == null || javaScriptChecksums.isEmpty()) { + logger.debug("Failed to calculate javaScript hash: {}", dependencyFile.getPath()); + } + for (Map.Entry entry : javaScriptChecksums.entrySet()) { + dependency.addChecksum(entry.getKey(), entry.getValue()); + } + } catch (Exception e) { + logger.warn("Failed to calculate javaScript hash for file: {}, error: {}", dependencyFile.getPath(), e.getMessage()); + logger.debug("Failed to calculate javaScript hash for file: {}, error: {}", dependencyFile.getPath(), e.getStackTrace()); + } + } + // other platform SHA1 + ChecksumUtils.calculateOtherPlatformSha1(dependency, dependencyFile); + // super hash + ChecksumUtils.calculateSuperHash(dependency, dependencyFile); + } catch (IOException e) { + logger.warn("Failed to create dependency " + filename + " to dependency list: {}", e.getMessage()); + dependency = null; + } + return dependency; + } + + /* --- Private methods --- */ + + private Collection extractCopyrights(File file) { + Collection copyrights = new ArrayList<>(); + try { + boolean commentBlock = false; + Iterator iterator = FileUtils.readLines(file).iterator(); + int lineIndex = 1; + while (iterator.hasNext()) { + // trim (duh..) + String line = iterator.next().trim(); + + // check if comment block + if (!commentBlock) { + for (Map.Entry entry : commentStartEndMap.entrySet()) { + String commentStart = entry.getKey(); + String commentEnd = entry.getValue(); + if (line.startsWith(commentStart)) { + if (line.contains(commentEnd)) { + commentBlock = false; + int endIndex = line.indexOf(commentEnd); + int commentLength = commentStart.length(); + if (endIndex >= commentLength) { + line = line.substring(commentLength, endIndex); + } else { + line = Constants.EMPTY_STRING; + } + } else { + commentBlock = true; + } + break; + } else if (line.contains(commentStart)) { + int startIndex = line.indexOf(commentStart); + if (line.contains(commentEnd)) { + int endIndex = line.indexOf(commentEnd); + if (startIndex < endIndex) { + commentBlock = false; + line = line.substring(startIndex, endIndex); + } + } else { + commentBlock = true; + line = line.substring(startIndex); + } + break; + } + } + } + + // check for one-line comments + String lowerCaseLine = line.toLowerCase(); + if ((commentBlock || line.startsWith("//") || line.startsWith(Constants.POUND)) + && (lowerCaseLine.contains(COPYRIGHT) || lowerCaseLine.contains(COPYRIGHT_SYMBOL) || lowerCaseLine.contains(COPYRIGHT_ASCII_SYMBOL))) { + // ignore lines that contain (c) and math signs (+, <, etc.) near it + // and ignore lines that contain © and have other invalid ascii symbols + if ((lowerCaseLine.contains(COPYRIGHT_SYMBOL) && isMathExpression(lowerCaseLine)) || + lowerCaseLine.contains(COPYRIGHT_ASCII_SYMBOL) && hasInvalidAsciiChars(lowerCaseLine)) { + continue; + } + line = cleanLine(line); + + if (line.toLowerCase().matches(TODO_PATTERN)) { + continue; + } + + StringBuilder sb = new StringBuilder(); + sb.append(line); + + // check if copyright continues to next line + boolean continuedToNextLine = false; + if (iterator.hasNext()) { + String copyrightOwner = null; + lowerCaseLine = line.toLowerCase(); + for (String copyrightText : COPYRIGHT_TEXTS) { + if (lowerCaseLine.startsWith(copyrightText)) { + copyrightOwner = line.substring(copyrightText.length()).trim(); + break; + } + } + + if (copyrightOwner != null) { + // check if copyright contains an alpha char (not just years) + if (!copyrightOwner.matches(COPYRIGHT_ALPHA_CHAR_REGEX)) { + // check if line has ending of comment block + for (String commentEnd : commentStartEndMap.values()) { + if (line.contains(commentEnd)) { + commentBlock = false; + break; + } + } + + // if still in comment block, read next line + if (commentBlock) { + String nextLine = cleanLine(iterator.next()); + sb.append(Constants.WHITESPACE); + sb.append(nextLine); + continuedToNextLine = true; + line = nextLine; + } + } + } + } + + // remove "all rights reserved" if exists + String copyright = sb.toString(); + String lowercaseCopyright = copyright.toLowerCase(); + if (lowercaseCopyright.contains(ALL_RIGHTS_RESERVED)) { + int startIndex = lowercaseCopyright.indexOf(ALL_RIGHTS_RESERVED); + int endIndex = startIndex + ALL_RIGHTS_RESERVED.length(); + if (endIndex == copyright.length()) { + copyright = copyright.substring(0, startIndex).trim(); + } else { + copyright = copyright.substring(0, startIndex).trim() + Constants.WHITESPACE + copyright.substring(endIndex).trim(); + } + } + copyrights.add(new CopyrightInfo(copyright, lineIndex)); + + if (continuedToNextLine) { + lineIndex++; + } + } + + // check if line has ending of comment block + for (String commentEnd : commentStartEndMap.values()) { + if (line.contains(commentEnd)) { + commentBlock = false; + break; + } + } + lineIndex++; + } + } catch (FileNotFoundException e) { + logger.warn("File not found: " + file.getPath()); + } catch (IOException e) { + logger.warn("Error reading file: " + file.getPath()); + } + + removeRedundantCopyrights(copyrights); + + return copyrights; + } + + private void removeRedundantCopyrights(Collection copyrights) { + if (copyrights.size() > 1) { + // check if exists at least one copyright with year + boolean hasCopyrightWithYear = false; + for (CopyrightInfo copyright : copyrights) { + if (copyright.getCopyright().matches(CONTAINS_YEAR_REGEX)) { + hasCopyrightWithYear = true; + break; + } + } + + if (hasCopyrightWithYear) { + Iterator iterator = copyrights.iterator(); + while (iterator.hasNext()) { + CopyrightInfo copyrightInfo = iterator.next(); + String copyright = copyrightInfo.getCopyright(); + + // remove regular lines found with the word 'copyright' but without year + // don't remove lines without year but have 'Copyright (C)' (probably an actual copyright reference) + if (!copyright.matches(CONTAINS_YEAR_REGEX) && !copyright.toLowerCase().contains(COPYRIGHT_SYMBOL)) { + iterator.remove(); + } + } + } + } + + // remove duplicate copyrights + Set copyTexts = new HashSet<>(); + Iterator iterator = copyrights.iterator(); + while (iterator.hasNext()) { + String lowerCaseCopyright = iterator.next().getCopyright().toLowerCase(); + if (copyTexts.contains(lowerCaseCopyright)) { + iterator.remove(); + } else { + copyTexts.add(lowerCaseCopyright); + } + } + } + + private boolean containsExcludedCopyright(DependencyInfo dependencyInfo) { + for (CopyrightInfo copyrightInfo : dependencyInfo.getCopyrights()) { + String lowerCaseCopyright = copyrightInfo.getCopyright().toLowerCase(); + for (String excludedCopyright : excludedCopyrights) { + if (lowerCaseCopyright.contains(excludedCopyright.toLowerCase())) { + return true; + } + } + } + return false; + } + + // check if lines with (c) are actual copyright references of simple code lines + private boolean isMathExpression(String line) { + String cleanLine = cleanLine(line).trim(); + boolean mathExpression = false; + if (cleanLine.startsWith(DEFINE)) { + return true; + } else if (cleanLine.matches(CODE_LINE_SUFFIX)) { + return true; + } + + // go forward + int index = cleanLine.indexOf(COPYRIGHT_SYMBOL); + for (int i = index + 1; i < cleanLine.length(); i++) { + char c = cleanLine.charAt(i); + if (c == Constants.OPEN_BRACKET || c == Constants.CLOSE_BRACKET || c == Constants.WHITESPACE_CHAR) { + continue; + } else if (MATH_SYMBOLS.contains(c)) { + mathExpression = true; + break; + } else { + break; + } + } + + // go backwards + if (mathExpression) { + for (int i = index - 1; i >= 0; i--) { + char c = cleanLine.charAt(i); + if (c == Constants.OPEN_BRACKET || c == Constants.CLOSE_BRACKET || c == Constants.WHITESPACE_CHAR) { + continue; + } else if (MATH_SYMBOLS.contains(c)) { + mathExpression = true; + break; + } else { + break; + } + } + } + return mathExpression; + } + + private boolean hasInvalidAsciiChars(String line) { + String cleanLine = cleanLine(line).trim(); + int invalidChars = 0; + for (int i = 0; i < cleanLine.length(); i++) { + char c = cleanLine.charAt(i); + if (c > MAX_VALID_CHAR_VALUE || c == Constants.QUESTION_MARK) { + invalidChars++; + } + if (invalidChars == MAX_INVALID_CHARS) { + return true; + } + } + return false; + } + + private String cleanLine(String line) { + return line.replace("/**", Constants.EMPTY_STRING).replace("/*", Constants.EMPTY_STRING) + .replace("*", Constants.EMPTY_STRING).replace(Constants.POUND, Constants.EMPTY_STRING) + .replace(Constants.FORWARD_SLASH, Constants.EMPTY_STRING).replace("\\t", Constants.EMPTY_STRING) + .replace("\\n", Constants.EMPTY_STRING).trim(); + } + + private void deleteFile(File file) { + if (file != null) { + try { + FileUtils.forceDelete(file); + } catch (IOException e) { + // do nothing + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/FileSystemScanner.java b/src/main/java/org/whitesource/agent/FileSystemScanner.java new file mode 100644 index 0000000..2c05047 --- /dev/null +++ b/src/main/java/org/whitesource/agent/FileSystemScanner.java @@ -0,0 +1,526 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.archive.ArchiveExtractor; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.DependencyResolutionService; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.MemoryUsageHelper; +import org.whitesource.fs.FSAConfiguration; +import org.whitesource.fs.FileSystemAgent; +import org.whitesource.fs.Main; +import org.whitesource.fs.StatusCode; +import org.whitesource.fs.configuration.AgentConfiguration; +import org.whitesource.fs.configuration.ResolverConfiguration; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * This class does the actual directory scanning, creates {@link DependencyInfo}s. + * + * @author tom.shapira + * @author anna.rozin + */ +public class FileSystemScanner { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(FileSystemAgent.class); + private String FSA_FILE = "**/*whitesource-fs-agent-*.*jar"; + + /* --- Private Members --- */ + + private final boolean isSeparateProjects; + private final AgentConfiguration agent; + private final boolean showProgressBar; + private boolean enableImpactAnalysis; + private ViaLanguage iaLanguage; + private DependencyResolutionService dependencyResolutionService; + private String sha1; + + /* --- Constructors --- */ + + public FileSystemScanner(ResolverConfiguration resolver, AgentConfiguration agentConfiguration, boolean enableImpactAnalysis) { + this.dependencyResolutionService = new DependencyResolutionService(resolver); + this.isSeparateProjects = dependencyResolutionService.isSeparateProjects(); + this.agent = agentConfiguration; + this.showProgressBar = agentConfiguration.isShowProgressBar(); + this.enableImpactAnalysis = enableImpactAnalysis; + } + + public FileSystemScanner(ResolverConfiguration resolver, AgentConfiguration agentConfiguration, boolean enableImpactAnalysis, ViaLanguage iaLanguage) { + this(resolver, agentConfiguration, enableImpactAnalysis); + this.iaLanguage = iaLanguage; + } + + /* --- Public methods --- */ + + /** + * This method is usually called from outside by different other tools + * + * @param scannerBaseDirs folders to scan + * @param scmConnector use scmConnector + * @param includes includes glob patterns + * @param excludes excludes glob patterns + * @param globCaseSensitive global case sensitive + * @param archiveExtractionDepth depth of recursive extraction + * @param archiveIncludes includes glob patterns for extraction + * @param archiveExcludes exclude glob patterns for extraction + * @param archiveFastUnpack use fast extraction + * @param followSymlinks use followSymlinks + * @param excludedCopyrights use excludedCopyrights + * @param partialSha1Match use partialSha1Match + * @return list of all the dependencies for project + */ + + @Deprecated + public List createProjects(List scannerBaseDirs, Map> appPathsToDependencyDirs, boolean scmConnector, + String[] includes, String[] excludes, boolean globCaseSensitive, int archiveExtractionDepth, + String[] archiveIncludes, String[] archiveExcludes, boolean archiveFastUnpack, boolean followSymlinks, + Collection excludedCopyrights, boolean partialSha1Match, String[] pythonRequirementsFileIncludes) { + Collection projects = createProjects(scannerBaseDirs, appPathsToDependencyDirs, scmConnector, includes, excludes, globCaseSensitive, archiveExtractionDepth, + archiveIncludes, archiveExcludes, archiveFastUnpack, followSymlinks, excludedCopyrights, partialSha1Match, + false, false, pythonRequirementsFileIncludes).keySet(); + return projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()); + } + + @Deprecated + public List createProjects(List scannerBaseDirs, boolean scmConnector, + String[] includes, String[] excludes, boolean globCaseSensitive, int archiveExtractionDepth, + String[] archiveIncludes, String[] archiveExcludes, boolean archiveFastUnpack, boolean followSymlinks, + Collection excludedCopyrights, boolean partialSha1Match, String[] pythonRequirementsFileIncludes) { + return createProjects(scannerBaseDirs, convertListDirsToMap(scannerBaseDirs), scmConnector, includes, excludes, globCaseSensitive, archiveExtractionDepth, + archiveIncludes, archiveExcludes, archiveFastUnpack, followSymlinks, excludedCopyrights, partialSha1Match, pythonRequirementsFileIncludes); + } + + @Deprecated + public Map> createProjects(List scannerBaseDirs, boolean hasScmConnector) { + return createProjects(scannerBaseDirs, convertListDirsToMap(scannerBaseDirs), hasScmConnector); + } + + @Deprecated + public Map> createProjects(List scannerBaseDirs, Map> appPathsToDependencyDirs, boolean hasScmConnector) { + return createProjects(scannerBaseDirs, appPathsToDependencyDirs, hasScmConnector, agent.getIncludes(), agent.getExcludes(), agent.getGlobCaseSensitive(), agent.getArchiveExtractionDepth(), + agent.getArchiveIncludes(), agent.getArchiveExcludes(), agent.isArchiveFastUnpack(), agent.isFollowSymlinks(), + agent.getExcludedCopyrights(), agent.isPartialSha1Match(), agent.isCalculateHints(), agent.isCalculateMd5(), agent.getPythonRequirementsFileIncludes()); + } + + @Deprecated + public Map> createProjects(List scannerBaseDirs, boolean scmConnector, + String[] includes, String[] excludes, boolean globCaseSensitive, int archiveExtractionDepth, + String[] archiveIncludes, String[] archiveExcludes, boolean archiveFastUnpack, boolean followSymlinks, + Collection excludedCopyrights, boolean partialSha1Match, boolean calculateHints, boolean calculateMd5, String[] pythonRequirementsFileIncludes) { + return createProjects(scannerBaseDirs, convertListDirsToMap(scannerBaseDirs), scmConnector, includes, excludes, globCaseSensitive, archiveExtractionDepth, archiveIncludes, archiveExcludes, + archiveFastUnpack, followSymlinks, excludedCopyrights, partialSha1Match, calculateHints, calculateMd5, pythonRequirementsFileIncludes); + } + + @Deprecated + public Map> createProjects(List scannerBaseDirs, Map> appPathsToDependencyDirs, boolean scmConnector, + String[] includes, String[] excludes, boolean globCaseSensitive, int archiveExtractionDepth, + String[] archiveIncludes, String[] archiveExcludes, boolean archiveFastUnpack, boolean followSymlinks, + Collection excludedCopyrights, boolean partialSha1Match, boolean calculateHints, + boolean calculateMd5, String[] pythonRequirementsFileIncludes) { + AgentConfiguration agentConfiguration = new AgentConfiguration(includes, excludes, new String[]{}, new String[]{}, archiveExtractionDepth, archiveIncludes, archiveExcludes, archiveFastUnpack, + followSymlinks, partialSha1Match, calculateHints, calculateMd5, showProgressBar, globCaseSensitive, false, excludedCopyrights, new String[]{}, new String[]{}, + pythonRequirementsFileIncludes, Constants.EMPTY_STRING); + ProjectConfiguration projectConfiguration = new ProjectConfiguration(agentConfiguration, scannerBaseDirs, appPathsToDependencyDirs, scmConnector); + return createProjects(projectConfiguration); + } + + public Map> createProjects(ProjectConfiguration projectConfiguration) { + MemoryUsageHelper.SystemStats systemStats = MemoryUsageHelper.getMemoryUsage(); + logger.debug(systemStats.toString()); + + // get canonical paths + Set pathsToScan = getCanonicalPaths(projectConfiguration.getScannerBaseDirs()); + + for (String appPath : projectConfiguration.getAppPathsToDependencyDirs().keySet()) { + projectConfiguration.getAppPathsToDependencyDirs().put(appPath, getCanonicalPaths(projectConfiguration.getAppPathsToDependencyDirs().get(appPath))); + } + + // todo: consider adding exit since this can be called from other components + //validateParams(archiveExtractionDepth, includes); + + // scan directories + int totalFiles = 0; + + String unpackDirectory; + boolean archiveExtraction = false; + // go over all base directories, look for archives + Map archiveToBaseDirMap = new HashMap<>(); + List archiveDirectories = new ArrayList<>(); + AgentConfiguration agentConfiguration = projectConfiguration.getAgentConfiguration(); + if (agentConfiguration.getArchiveExtractionDepth() > 0) { + ArchiveExtractor archiveExtractor = new ArchiveExtractor(agentConfiguration.getArchiveIncludes(), agentConfiguration.getArchiveExcludes(), + agentConfiguration.getExcludes(), agentConfiguration.isArchiveFastUnpack()); + logger.info("Starting Archive Extraction (may take a few minutes)"); + for (String scannerBaseDir : new LinkedHashSet<>(pathsToScan)) { + unpackDirectory = archiveExtractor.extractArchives(scannerBaseDir, agentConfiguration.getArchiveExtractionDepth(), archiveDirectories); + if (unpackDirectory != null) { + archiveExtraction = true; + String parentFileUrl = new File(scannerBaseDir).getParent(); + logger.debug("Unpack directory: {}, parent file: {}", unpackDirectory, parentFileUrl); + archiveToBaseDirMap.put(unpackDirectory, parentFileUrl); + pathsToScan.add(unpackDirectory); + if (!projectConfiguration.getAppPathsToDependencyDirs().containsKey(FSAConfiguration.DEFAULT_KEY)) { + projectConfiguration.getAppPathsToDependencyDirs().put(FSAConfiguration.DEFAULT_KEY, new HashSet<>()); + } + projectConfiguration.getAppPathsToDependencyDirs().get(FSAConfiguration.DEFAULT_KEY).add(unpackDirectory); + } + } + } + + // create dependencies from files - first project is always the default one + logger.info("Starting analysis"); + // Create LinkedHashMap in order to save the order of the projects. In this way the first project will be always the main project. + Map allProjects = new LinkedHashMap<>(); + Map> allProjectsToViaComponents = new LinkedHashMap<>(); + AgentProjectInfo mainProject = new AgentProjectInfo(); + allProjects.put(mainProject, null); + allProjectsToViaComponents.put(mainProject, new LinkedList<>()); + String[] excludes = agentConfiguration.getExcludes(); + + logger.info("Scanning directories {} for matching Files (may take a few minutes)", pathsToScan); + logger.info("Included file types: {}", String.join(Constants.COMMA, agentConfiguration.getIncludes())); + logger.info("Excluded file types: {}", String.join(Constants.COMMA, agentConfiguration.getExcludes())); + String[] resolversIncludesPattern = createResolversIncludesPattern(dependencyResolutionService.getDependencyResolvers()); + + Map> fileMapBeforeResolve = new FilesUtils().fillFilesMap(pathsToScan, resolversIncludesPattern, agentConfiguration.getExcludes(), + agentConfiguration.isFollowSymlinks(), agentConfiguration.getGlobCaseSensitive()); + Set allFiles = fileMapBeforeResolve.entrySet().stream().flatMap(folder -> folder.getValue().stream()).collect(Collectors.toSet()); + + final int[] totalDependencies = {0}; + boolean isIgnoreSourceFiles = false; + if (enableImpactAnalysis && iaLanguage != null) { + for (String appPath : projectConfiguration.getAppPathsToDependencyDirs().keySet()) { + if (!appPath.equals(FSAConfiguration.DEFAULT_KEY)) { + if ((projectConfiguration.getAppPathsToDependencyDirs().get(appPath)).iterator().next() != null) { + String pojoAppPath = appPath; + allProjectsToViaComponents.get(allProjects.keySet().stream().findFirst().get()).add(new ViaComponents(pojoAppPath, iaLanguage)); + } + } + } + // the 'allFiles' collection is derived from the manifest-files of each resolver - + // therefore no need to check again if the files in that collection match the manifest-files of each resolver + } else if (allFiles.size() > 0) {//(dependencyResolutionService != null && dependencyResolutionService.shouldResolveDependencies(allFiles)) { + logger.info("Attempting to resolve dependencies"); + isIgnoreSourceFiles = dependencyResolutionService.isIgnoreSourceFiles(); + + // get all resolution results + Collection resolutionResults = new ArrayList<>(); + for (String appPath : projectConfiguration.getAppPathsToDependencyDirs().keySet()) { + ViaComponents viaComponents = null; + ViaLanguage impactAnalysisLanguage = null; + Collection resolutionResult = new LinkedList<>(); + LinkedList pathsList = new LinkedList<>(); + pathsList.addAll(projectConfiguration.getAppPathsToDependencyDirs().get(appPath)); + if ((appPath.equals(FSAConfiguration.DEFAULT_KEY) && projectConfiguration.getAppPathsToDependencyDirs().keySet().size() == 1) || + (!appPath.equals(FSAConfiguration.DEFAULT_KEY) && projectConfiguration.getAppPathsToDependencyDirs().keySet().size() > 1)) { + resolutionResult = dependencyResolutionService.resolveDependencies(pathsList, agentConfiguration.getExcludes()); + } + if (resolutionResult.size() == 1 && !appPath.equals(FSAConfiguration.DEFAULT_KEY)) { + DependencyType dependencyType = resolutionResult.stream().findFirst().get().getDependencyType(); + if (dependencyType == null) { + break; + } else { + // validate scanned language and set the + switch (dependencyType) { + case NPM: + case BOWER: + impactAnalysisLanguage = ViaLanguage.JAVA_SCRIPT; + break; + case MAVEN: + case GRADLE: + impactAnalysisLanguage = ViaLanguage.JAVA; + break; + default: + if (enableImpactAnalysis) { + logger.error("Effective Usage Analysis will not run if the system cannot locate a valid dependency manager and the " + + "-iaLanguage parameter is not specified. In order to run Effective Usage Analysis without a dependency manager specify -iaLanguage java"); + Main.exit(StatusCode.ERROR.getValue()); + //// TODO: 8/28/2018 as a result of WSE-765 exit using function from main. function signature should be change to throw an exception + } + break; + } + } + } else if (resolutionResult.size() > 1 && enableImpactAnalysis) { + logger.info("Effective Usage Analysis will not run if an unsupported resolver is active. Verify that non-supported resolvers are not active"); + Main.exit(StatusCode.ERROR.getValue()); + } + if (impactAnalysisLanguage != null) { + viaComponents = new ViaComponents(appPath, impactAnalysisLanguage); + } + // TODO: Check why is result = null in the loop + resolutionResult.removeIf(Objects::isNull); + for (ResolutionResult result : resolutionResult) { + Map projects = result.getResolvedProjects(); + Collection dependenciesToVia = new ArrayList<>(); + for (Map.Entry project : projects.entrySet()) { + Collection dependencies = project.getKey().getDependencies(); + dependenciesToVia.addAll(dependencies); + // do not add projects with no dependencies + if (!dependencies.isEmpty()) { + AgentProjectInfo currentProject; + + // if it is single project threat it as the main + if ((((DependencyType.MAVEN.equals(result.getDependencyType()) && (!dependencyResolutionService.isMavenAggregateModules() || !dependencyResolutionService.isSbtAggregateModules())) || + (DependencyType.GRADLE.equals(result.getDependencyType()) && !dependencyResolutionService.isGradleAggregateModules()) || + (DependencyType.HEX.equals(result.getDependencyType()) && !dependencyResolutionService.isHexAggregateModules()))) && + result.getResolvedProjects().size() > 1) { + allProjects.put(project.getKey(), project.getValue()); + LinkedList listToNewProject = new LinkedList<>(); + if (impactAnalysisLanguage != null) { + listToNewProject.add(viaComponents); + } + allProjectsToViaComponents.put(project.getKey(), listToNewProject); + } else { + currentProject = allProjects.keySet().stream().findFirst().get(); + currentProject.getDependencies().addAll(project.getKey().getDependencies()); + if (impactAnalysisLanguage != null) { + allProjectsToViaComponents.get(allProjects.keySet().stream().findFirst().get()).add(viaComponents); + } + } + impactAnalysisLanguage = null; + totalDependencies[0] += dependencies.size(); + List usedSha1 = new LinkedList<>(); + dependencies.forEach(dependency -> increaseCount(dependency, totalDependencies, usedSha1)); + } + } + if (viaComponents != null) { + viaComponents.getDependencies().addAll(dependenciesToVia); + } + } + resolutionResults.addAll(resolutionResult); + } + + resolutionResults.stream().forEach(resolutionResult -> logger.debug("total resolved projects = {}", resolutionResult.getResolvedProjects().size())); + logger.info(MessageFormat.format("Total dependencies found: {0}", totalDependencies[0])); + + // merge additional excludes + Set allExcludes = resolutionResults.stream().flatMap(resolution -> resolution.getExcludes().stream()).collect(Collectors.toSet()); + allExcludes.addAll(Arrays.stream(agentConfiguration.getExcludes()).collect(Collectors.toList())); + + // change the original excludes with the merged values + excludes = new String[allExcludes.size()]; + excludes = allExcludes.toArray(excludes); + dependencyResolutionService = null; + } + + String[] excludesExtended = excludeFileSystemAgent(excludes); + logger.info("Scanning directories {} for matching Files (may take a few minutes)", pathsToScan); + Map> fileMap = new FilesUtils().fillFilesMap(pathsToScan, agentConfiguration.getIncludes(), excludesExtended, + agentConfiguration.isFollowSymlinks(), agentConfiguration.getGlobCaseSensitive()); + long filesCount = fileMap.entrySet().stream().flatMap(folder -> folder.getValue().stream()).count(); + totalFiles += filesCount; + logger.info(MessageFormat.format("Total files found according to the includes/excludes pattern: {0}", totalFiles)); + DependencyCalculator dependencyCalculator = new DependencyCalculator(showProgressBar); + final Collection filesDependencies = new LinkedList<>(); + + if (!isIgnoreSourceFiles) { + filesDependencies.addAll(dependencyCalculator.createDependencies( + projectConfiguration.isScmConnector(), totalFiles, fileMap, agentConfiguration.getExcludedCopyrights(), + agentConfiguration.isPartialSha1Match(), agentConfiguration.isCalculateHints(), + agentConfiguration.isCalculateMd5())); + } + + if (allProjects.size() == 1) { + AgentProjectInfo project = allProjects.keySet().stream().findFirst().get(); + project.getDependencies().addAll(filesDependencies); + /// TODO: 8/14/2018 support multi module project with via + /* if (enableImpactAnalysis) { + for (LinkedList viaComponentsList : allProjectsToViaComponents.values()) { + for (ViaComponents viaComponents : viaComponentsList) { + for (DependencyInfo dependencyInfo : filesDependencies) { + if (dependencyInfo.getSystemPath().equals(viaComponents.getAppPath())) { + viaComponents.getDependencies().add(dependencyInfo); + } + } + } + } + }*/ + } else { + // Sort the projects by length of paths (from the longest to the shortest) in order to add filesDependencies to the most appropriate project + // Example: project1 path: C:\Users\file\Data; project2 path: C:\Users\file\Data\folder; file dependency path: C:\Users\file\Data\folder\a.jar + // Before sorting, the file dependency will be in project1. After sorting, the file dependency will be in project2. + List> entriesList = new ArrayList<>(); + allProjects.entrySet().forEach(entry -> { + if (entry.getValue() != null) { + entriesList.add(entry); + } + }); + entriesList.sort(Map.Entry.comparingByValue()); + Collections.reverse(entriesList); + Map result = new LinkedHashMap<>(); + entriesList.forEach(entry -> result.put(entry.getKey(), entry.getValue())); + + // remove files from handled projects + result.entrySet().forEach(project -> { + Collection projectDependencies = filesDependencies.stream() + .filter(dependencyInfo -> project.getValue() != null && dependencyInfo.getSystemPath().contains(project.getValue().toString())).collect(Collectors.toList()); + project.getKey().getDependencies().addAll(projectDependencies); + filesDependencies.removeAll(projectDependencies); + }); + + // create new projects if necessary + if (!isIgnoreSourceFiles && filesDependencies.size() > 0) { + projectConfiguration.getScannerBaseDirs().stream().forEach(directory -> { + List subDirectories; + // check all folders + + String[] includesAll = {Constants.PATTERN}; + subDirectories = new FilesUtils().getSubDirectories(directory, includesAll, null, agentConfiguration.isFollowSymlinks(), + agentConfiguration.getGlobCaseSensitive()); + subDirectories.forEach(subFolder -> { + if (filesDependencies.size() > 0) { + List projectDependencies = filesDependencies.stream(). + filter(dependencyInfo -> dependencyInfo.getSystemPath().contains(subFolder.toString())).collect(Collectors.toList()); + if (!projectDependencies.isEmpty()) { + AgentProjectInfo subProject; + if (isSeparateProjects) { + subProject = new AgentProjectInfo(); + allProjects.put(subProject, null); + allProjectsToViaComponents.put(subProject, new LinkedList<>()); + subProject.setCoordinates(new Coordinates(null, subFolder.toFile().getName(), null)); + } else { + subProject = allProjects.entrySet().stream().findFirst().get().getKey(); + } + subProject.getDependencies().addAll(filesDependencies); + filesDependencies.removeAll(projectDependencies); + } + } + }); + }); + // Add the rest of the files dependencies to the main project + if (!filesDependencies.isEmpty()) { + AgentProjectInfo subProject = allProjects.entrySet().stream().findFirst().get().getKey(); + subProject.getDependencies().addAll(filesDependencies); + } + } + } + + for (AgentProjectInfo innerProject : allProjects.keySet()) { + // replace temp folder name with base dir + for (DependencyInfo dependencyInfo : innerProject.getDependencies()) { + String systemPath = dependencyInfo.getSystemPath(); + if (systemPath == null) { + logger.debug("Dependency {} has no system path", dependencyInfo.getArtifactId()); + } else { + for (String key : archiveToBaseDirMap.keySet()) { + if (systemPath.contains(key) && archiveExtraction) { + String newSystemPath = systemPath.replace(key, archiveToBaseDirMap.get(key)).replaceAll(ArchiveExtractor.DEPTH_REGEX, Constants.EMPTY_STRING); + logger.debug("Original system path: {}, new system path: {}, key: {}", systemPath, newSystemPath, key); + dependencyInfo.setSystemPath(newSystemPath); + break; + } + } + } + } + } + + // delete all archive temp folders + if (!archiveDirectories.isEmpty()) { + for (String archiveDirectory : archiveDirectories) { + File directory = new File(archiveDirectory); + if (directory.exists()) { + FileUtils.deleteQuietly(directory); + } + } + } + logger.info("Finished analyzing Files"); + systemStats = MemoryUsageHelper.getMemoryUsage(); + logger.debug(systemStats.toString()); + // add dependencies to project in case of pojo project + if (enableImpactAnalysis && iaLanguage != null) { + AgentProjectInfo agentProjectInfo = allProjectsToViaComponents.keySet().iterator().next(); + allProjectsToViaComponents.get(agentProjectInfo).getFirst().getDependencies().addAll( + agentProjectInfo.getDependencies()); + } + return allProjectsToViaComponents; + } + + /* --- Private methods --- */ + + private String[] createResolversIncludesPattern(Collection dependencyResolvers) { + Collection resultIncludes = new ArrayList<>(); + // TODO - check if can be done with lambda + for (AbstractDependencyResolver dependencyResolver : dependencyResolvers) { + for (String manifestFile : dependencyResolver.getManifestFiles()){ + if (!manifestFile.isEmpty()) { + resultIncludes.add(Constants.PATTERN + manifestFile); + } + } + } + String[] resultArray = new String[resultIncludes.size()]; + resultIncludes.toArray(resultArray); + return resultArray; + } + + private Map> convertListDirsToMap(List scannerBaseDirs) { + Map> appPathsToDependencyDirs = new HashMap<>(); + appPathsToDependencyDirs.put(FSAConfiguration.DEFAULT_KEY, new HashSet<>()); + for (String dir : scannerBaseDirs) { + appPathsToDependencyDirs.get(FSAConfiguration.DEFAULT_KEY).add(dir); + } + return appPathsToDependencyDirs; + } + + private Set getCanonicalPaths(Collection scannerBaseDirs) { + // use canonical paths to resolve '.' in path + Set pathsToScan = new HashSet<>(); + for (String path : scannerBaseDirs) { + try { + pathsToScan.add(new File(path).getCanonicalPath()); + } catch (IOException e) { + // use the given path as-is + logger.debug("Error finding the canonical path of {}", path); + pathsToScan.add(path); + } + } + return pathsToScan; + } + + private void increaseCount(DependencyInfo dependency, int[] totalDependencies, List usedSha1) { + sha1 = dependency.getSha1(); + if (usedSha1.contains(sha1)) { + return; + } + usedSha1.add(sha1); + totalDependencies[0] += dependency.getChildren().size(); + dependency.getChildren().forEach(dependencyInfo -> increaseCount(dependencyInfo, totalDependencies, usedSha1)); + } + + private String[] excludeFileSystemAgent(String[] excludes) { + String[] allExcludes = excludes == null ? new String[0] : excludes; + String[] excludesFSA = new String[allExcludes.length + 1]; + System.arraycopy(allExcludes, 0, excludesFSA, 0, allExcludes.length); + excludesFSA[allExcludes.length] = FSA_FILE; + return excludesFSA; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/ProjectConfiguration.java b/src/main/java/org/whitesource/agent/ProjectConfiguration.java new file mode 100644 index 0000000..339dabc --- /dev/null +++ b/src/main/java/org/whitesource/agent/ProjectConfiguration.java @@ -0,0 +1,69 @@ +package org.whitesource.agent; + +import org.whitesource.fs.configuration.AgentConfiguration; + +import java.util.*; + +/** + * @author chen.luigi + */ +public class ProjectConfiguration { + + /* --- Private Members --- */ + + private AgentConfiguration agentConfiguration; + private List scannerBaseDirs; + private Map> appPathsToDependencyDirs; + private boolean scmConnector; + + + /* --- Constructors --- */ + + public ProjectConfiguration(AgentConfiguration agentConfiguration) { + this.agentConfiguration = agentConfiguration; + scannerBaseDirs = new LinkedList<>(); + appPathsToDependencyDirs = new HashMap<>(); + this.scmConnector = false; + } + + public ProjectConfiguration(AgentConfiguration agentConfiguration, List scannerBaseDirs, Map> appPathsToDependencyDirs, boolean scmConnector) { + this.agentConfiguration = agentConfiguration; + this.scannerBaseDirs = scannerBaseDirs; + this.appPathsToDependencyDirs = appPathsToDependencyDirs; + this.scmConnector = scmConnector; + } + + /* --- Getters / Setters --- */ + + public AgentConfiguration getAgentConfiguration() { + return agentConfiguration; + } + + public void setAgentConfiguration(AgentConfiguration agentConfiguration) { + this.agentConfiguration = agentConfiguration; + } + + public List getScannerBaseDirs() { + return scannerBaseDirs; + } + + public void setScannerBaseDirs(List scannerBaseDirs) { + this.scannerBaseDirs = scannerBaseDirs; + } + + public Map> getAppPathsToDependencyDirs() { + return appPathsToDependencyDirs; + } + + public void setAppPathsToDependencyDirs(Map> appPathsToDependencyDirs) { + this.appPathsToDependencyDirs = appPathsToDependencyDirs; + } + + public boolean isScmConnector() { + return scmConnector; + } + + public void setScmConnector(boolean scmConnector) { + this.scmConnector = scmConnector; + } +} diff --git a/src/main/java/org/whitesource/agent/ProjectsSender.java b/src/main/java/org/whitesource/agent/ProjectsSender.java new file mode 100644 index 0000000..b215f8b --- /dev/null +++ b/src/main/java/org/whitesource/agent/ProjectsSender.java @@ -0,0 +1,479 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.api.dispatch.*; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.client.WhitesourceService; +import org.whitesource.agent.client.WssServiceException; +import org.whitesource.agent.report.OfflineUpdateRequest; +import org.whitesource.agent.report.PolicyCheckReport; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.Pair; +import org.whitesource.contracts.PluginInfo; +import org.whitesource.fs.LogMapAppender; +import org.whitesource.fs.Main; +import org.whitesource.fs.ProjectsDetails; +import org.whitesource.fs.StatusCode; +import org.whitesource.fs.configuration.OfflineConfiguration; +import org.whitesource.fs.configuration.RequestConfiguration; +import org.whitesource.fs.configuration.SenderConfiguration; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.stream.Collectors; + +/** + * Class for sending projects for all WhiteSource command line agents. + * + * @author Itai Marko + * @author tom.shapira + * @author anna.rozin + */ +public class ProjectsSender { + /* --- Static members --- */ + private static final String DATE_FORMAT = "HH:mm:ss"; + public static final String PROJECT_URL_PREFIX = "Wss/WSS.html#!project;id="; + protected static final int MAX_LOG_EVENTS = 1000; + /* --- Members --- */ + private final Logger logger = LoggerFactory.getLogger(ProjectsSender.class); + private final SenderConfiguration senderConfig; + private final OfflineConfiguration offlineConfig; + private final RequestConfiguration requestConfig; + private final PluginInfo pluginInfo; + protected StatusCode prepStepStatusCode = StatusCode.SUCCESS; + + /* --- Constructors --- */ + + public ProjectsSender(SenderConfiguration senderConfig, OfflineConfiguration offlineConfig, RequestConfiguration requestConfig, PluginInfo pluginInfo) { + this.senderConfig = senderConfig; + this.offlineConfig = offlineConfig; + this.requestConfig = requestConfig; + this.pluginInfo = pluginInfo; + } + + /* --- Public methods --- */ + + public Pair sendRequest(ProjectsDetails projectsDetails) { + // send request + logger.info("Initializing WhiteSource Client"); + Collection projects = projectsDetails.getProjects(); + + if (checkDependenciesUpbound(projects)) { + return new Pair<>("Number of dependencies exceeded the maximum supported", StatusCode.SERVER_FAILURE); + } + + WhitesourceService service = createService(); + String resultInfo = Constants.EMPTY_STRING; + if (offlineConfig.isOffline()) { + resultInfo = offlineUpdate(service, projects); + return new Pair<>(resultInfo, this.prepStepStatusCode); + } else { + // update type + UpdateType updateType = UpdateType.OVERRIDE; + String updateTypeValue = senderConfig.getUpdateTypeValue(); + try { + updateType = UpdateType.valueOf(updateTypeValue); + } catch (Exception e) { + logger.info("Invalid value {} for updateType, defaulting to {}", updateTypeValue, UpdateType.OVERRIDE); + } + logger.info("UpdateType set to {} ", updateTypeValue); + + StatusCode statusCode = StatusCode.SUCCESS; + if (senderConfig.isEnableImpactAnalysis()) { + runViaAnalysis(projectsDetails, service); + } else if (!senderConfig.isEnableImpactAnalysis()) { + //todo return logs when needed would be enabled for all WSE-342 + } + int retries = senderConfig.getConnectionRetries(); + while (retries-- > -1) { + try { + statusCode = checkPolicies(service, projects); + if (senderConfig.isUpdateInventory()) { + if (statusCode == StatusCode.SUCCESS || (senderConfig.isForceUpdate() && senderConfig.isForceUpdateFailBuildOnPolicyViolation())) { + resultInfo = update(service, projects); + } + } + break; + } catch (WssServiceException e) { + if (e.getCause() != null && + e.getCause().getClass().getCanonicalName().substring(0, + e.getCause().getClass().getCanonicalName().lastIndexOf(Constants.DOT)).equals(Constants.JAVA_NETWORKING)) { + statusCode = StatusCode.CONNECTION_FAILURE; + logger.error("Trying " + (retries + 1) + " more time" + (retries != 0 ? "s" : Constants.EMPTY_STRING)); + } else { + statusCode = StatusCode.SERVER_FAILURE; + retries = -1; + } + resultInfo = "Failed to send request to WhiteSource server: " + e.getMessage(); + logger.error(resultInfo, e.getMessage()); + logger.debug(resultInfo, e); + if (retries > -1) { + try { + Thread.sleep(senderConfig.getConnectionRetriesIntervals()); + } catch (InterruptedException e1) { + logger.error("Failed to sleep while retrying to connect to server " + e1.getMessage(), e1); + } + } + String requestToken = e.getRequestToken(); + if (StringUtils.isNotBlank(requestToken)) { + resultInfo += Constants.NEW_LINE + "Support token: " + requestToken; + logger.info("Support token: {}", requestToken); + } + } + } + if (service != null) { + service.shutdown(); + } + if (statusCode == StatusCode.SUCCESS) { + return new Pair<>(resultInfo, this.prepStepStatusCode); + } + return new Pair<>(resultInfo, statusCode); + } + } + + private void runViaAnalysis(ProjectsDetails projectsDetails, WhitesourceService service) { + try { + Class vulnerabilitiesAnalysisClass = Class.forName("whitesource.analysis.vulnerabilities.VulnerabilitiesAnalysis"); + Method getAnalysisMethod + = vulnerabilitiesAnalysisClass.getMethod("getAnalysis", String.class, int.class); + Object vulnerabilitiesAnalysis = null; + for (AgentProjectInfo project : projectsDetails.getProjectToViaComponents().keySet()) { + // check language for scan according to user file + LinkedList viaComponentsList = projectsDetails.getProjectToViaComponents().get(project); + if (viaComponentsList.size() == 0) { + logger.error("Effective Usage Analysis will not run if 0 dependencies are found. Check that the -d parameter specifies a valid project path"); + Main.exit(StatusCode.ERROR.getValue()); + //// TODO: 8/28/2018 as a result of WSE-765 exit using function from main. function signature should be change to throw an exception + } + for (ViaComponents viaComponents : viaComponentsList) { + logger.info("Starting VIA impact analysis"); + checkDependenciesSha1(viaComponents); + String appPath = viaComponents.getAppPath(); + ViaLanguage language = viaComponents.getLanguage(); + try { + vulnerabilitiesAnalysis = getAnalysisMethod.invoke(null, language.toString(), requestConfig.getViaAnalysisLevel()); + // set app path for java script + if (language.equals(ViaLanguage.JAVA_SCRIPT)) { + int lastIndex = appPath.lastIndexOf(Constants.BACK_SLASH) != -1 ? appPath.lastIndexOf(Constants.BACK_SLASH) : appPath.lastIndexOf(Constants.FORWARD_SLASH); + appPath = appPath.substring(0, lastIndex); + } + if (vulnerabilitiesAnalysis != null) { + AgentProjectInfo projectToServer = new AgentProjectInfo(); + projectToServer.setDependencies(Lists.newArrayList(project.getDependencies())); + projectToServer.setProjectSetupDescription(project.getProjectSetupDescription()); + projectToServer.setCoordinates(project.getCoordinates()); + projectToServer.setProjectToken(project.getProjectToken()); + projectToServer.setProjectSetupStatus(project.getProjectSetupStatus()); + projectToServer.setParentCoordinates(project.getParentCoordinates()); + Class fsaAgentServerClass = Class.forName("whitesource.analysis.server.FSAgentServer"); + Object server = fsaAgentServerClass.getConstructor(AgentProjectInfo.class, WhitesourceService.class, String.class, String.class).newInstance( + projectToServer, service, requestConfig.getApiToken(), requestConfig.getUserKey()); + logger.info("Starting analysis for: {}", appPath); + Class serverClass = Class.forName("whitesource.analysis.server.Server"); + Method runAnalysis = vulnerabilitiesAnalysisClass.getDeclaredMethod("runAnalysis", serverClass, String.class, Collection.class, Boolean.class); + runAnalysis.invoke(vulnerabilitiesAnalysis, server, appPath, project.getDependencies(), Boolean.valueOf(requestConfig.getViaDebug())); + logger.info("Got impact analysis result from server"); + } + } catch (InvocationTargetException e) { + logger.error("Failed to run VIA impact analysis {}", e.getTargetException().getMessage()); + } catch (Exception e) { + logger.error("Failed to run VIA impact analysis {}", e.getMessage()); + } + } + } + } catch (NoSuchMethodException e) { + logger.error("Failed to run VIA impact analysis, couldn't find method {}", e.getMessage()); + } catch (ClassNotFoundException e) { + logger.error("Failed to run VIA impact analysis, couldn't find class {}", e.getMessage()); + } + } + + private void checkDependenciesSha1(ViaComponents viaComponents) { + Collection dependencyWithoutSha1 = new LinkedList<>(); + int unknownDependencyCounter = 0; + for (DependencyInfo dependencyInfo : viaComponents.getDependencies()) { + if (StringUtils.isEmpty(dependencyInfo.getSha1())) { + unknownDependencyCounter++; + dependencyWithoutSha1.add(dependencyInfo); + } + } + if (unknownDependencyCounter > 0) { + if (requestConfig.isRequireKnownSha1()) { + logger.warn("The system found {} with an unknown SHA1 value. Default processing was terminated.", unknownDependencyCounter); + printDependenciesWithoutSha1(dependencyWithoutSha1); + Main.exit(StatusCode.ERROR.getValue()); + } else { + logger.warn("The system found {} with an unknown SHA1 value. Default processing termination was overridden by parameter.", unknownDependencyCounter); + printDependenciesWithoutSha1(dependencyWithoutSha1); + } + } + } + + private void printDependenciesWithoutSha1(Collection dependencyWithoutSha1) { + logger.warn("Found dependencies with an unknown SHA1 value:"); + for (DependencyInfo dependencyInfo : dependencyWithoutSha1) { + logger.warn(new Coordinates(dependencyInfo.getGroupId(), dependencyInfo.getArtifactId(), dependencyInfo.getVersion()).toString()); + } + } + + private boolean checkDependenciesUpbound(Collection projects) { + int numberOfDependencies = projects.stream().map(x -> x.getDependencies()).mapToInt(x -> x.size()).sum(); + if (numberOfDependencies > Constants.MAX_NUMBER_OF_DEPENDENCIES) { + logger.warn("Number of dependencies: {} exceeded the maximum supported: {}", numberOfDependencies, Constants.MAX_NUMBER_OF_DEPENDENCIES); + return true; + } + return false; + } + + protected WhitesourceService createService() { + logger.info("Service URL is " + senderConfig.getServiceUrl()); + boolean setProxy = false; + if (StringUtils.isNotBlank(senderConfig.getProxyHost()) || !offlineConfig.isOffline()) { + setProxy = true; + } + int connectionTimeoutMinutes = senderConfig.getConnectionTimeOut(); + final WhitesourceService service = new WhitesourceService(pluginInfo.getAgentType(), pluginInfo.getAgentVersion(), pluginInfo.getPluginVersion(), + senderConfig.getServiceUrl(), setProxy, connectionTimeoutMinutes, senderConfig.isIgnoreCertificateCheck()); + if (StringUtils.isNotBlank(senderConfig.getProxyHost())) { + service.getClient().setProxy(senderConfig.getProxyHost(), senderConfig.getProxyPort(), senderConfig.getProxyUser(), senderConfig.getProxyPassword()); + } + return service; + } + + private StatusCode checkPolicies(WhitesourceService service, Collection projects) throws WssServiceException { + boolean policyCompliance = true; + if (senderConfig.isCheckPolicies() || !senderConfig.isUpdateInventory()) { + logger.info("Checking policies"); + CheckPolicyComplianceResult checkPoliciesResult; + if (senderConfig.isSendLogsToWss()) { + String logData = getLogData(); + checkPoliciesResult = service.checkPolicyCompliance(requestConfig.getApiToken(), requestConfig.getProductName(), + requestConfig.getProductVersion(), projects, senderConfig.isForceCheckAllDependencies(), requestConfig.getUserKey(), + requestConfig.getRequesterEmail(), logData, requestConfig.getProductToken()); + } else { + checkPoliciesResult = service.checkPolicyCompliance(requestConfig.getApiToken(), requestConfig.getProductName(), + requestConfig.getProductVersion(), projects, senderConfig.isForceCheckAllDependencies(), requestConfig.getUserKey(), + requestConfig.getRequesterEmail(), null, requestConfig.getProductToken()); + } + if (checkPoliciesResult.hasRejections()) { + if (senderConfig.isForceUpdate() && senderConfig.isUpdateInventory()) { + logger.info("Some dependencies violate open source policies, however all were force " + + "updated to organization inventory."); + if (senderConfig.isForceUpdateFailBuildOnPolicyViolation()) { + policyCompliance = false; + } + } else if (!senderConfig.isUpdateInventory()) { + logger.info("Some dependencies did not conform with open source policies, review report for details"); + policyCompliance = false; + } else { + logger.info("Some dependencies did not conform with open source policies, review report for details"); + logger.info("=== UPDATE ABORTED ==="); + policyCompliance = false; + } + } else { + logger.info("All dependencies conform with open source policies."); + } + String requestToken = checkPoliciesResult.getRequestToken(); + if (StringUtils.isNotBlank(requestToken)) { + logger.info("Check Policies Support Token: {}", requestToken); + } + try { + // generate report + PolicyCheckReport report = new PolicyCheckReport(checkPoliciesResult); + File outputDir = new File(offlineConfig.getWhiteSourceFolderPath()); + report.generate(outputDir, false); + report.generateJson(outputDir); + logger.info("Policies report generated successfully"); + } catch (IOException e) { + logger.error("Error generating check policies report: " + e.getMessage(), e); + } + } + return policyCompliance ? StatusCode.SUCCESS : StatusCode.POLICY_VIOLATION; + } + + protected String update(WhitesourceService service, Collection projects) throws WssServiceException { + logger.info("Sending Update"); + UpdateInventoryResult updateResult; + //-------------------------------- + if (requestConfig.getViaDebug().equals("SAVE") || Boolean.valueOf(requestConfig.getViaDebug())) { + saveRequestToFile(projects); + } + //-------------------------------- + if (senderConfig.isSendLogsToWss()) { + String logData = getLogData(); + updateResult = service.update(requestConfig.getApiToken(), requestConfig.getRequesterEmail(), UpdateType.valueOf(senderConfig.getUpdateTypeValue()), + requestConfig.getProductName(), requestConfig.getProductVersion(), projects, requestConfig.getUserKey(), + logData, requestConfig.getScanComment(), requestConfig.getProductToken()); + + } else { + updateResult = service.update(requestConfig.getApiToken(), requestConfig.getRequesterEmail(), UpdateType.valueOf(senderConfig.getUpdateTypeValue()), + requestConfig.getProductName(), requestConfig.getProductVersion(), projects, requestConfig.getUserKey(), null, + requestConfig.getScanComment(), requestConfig.getProductToken()); + } + String resultInfo = logResult(updateResult); + // remove line separators + resultInfo = resultInfo.replace(System.lineSeparator(), Constants.EMPTY_STRING); + return resultInfo; + } + + private void saveRequestToFile(Collection projects) { + String fileName = "jsonOut" + Constants.DASH + requestConfig.getProductName() + Constants.DASH + + requestConfig.getProjectName() + ".json"; + RequestFactory requestFactory = new RequestFactory(pluginInfo.getAgentType(), pluginInfo.getAgentVersion(), pluginInfo.getPluginVersion()); + String updateJson = new Gson().toJson(requestFactory.newUpdateInventoryRequest(requestConfig.getApiToken(), + UpdateType.valueOf(senderConfig.getUpdateTypeValue()), requestConfig.getRequesterEmail(), + requestConfig.getProductName(), requestConfig.getProductVersion(), projects, + requestConfig.getUserKey(), (String) null, (String) null, (String) requestConfig.getProductToken())); + Path path = Paths.get(fileName); + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + writer.write(updateJson); + } catch (Exception e) { + logger.debug("couldn't create via debug file {}", e.getMessage()); + } + } + + private String offlineUpdate(WhitesourceService service, Collection projects) { + String resultInfo = Constants.EMPTY_STRING; + logger.info("Generating offline update request"); + // generate offline request + UpdateInventoryRequest updateRequest = service.offlineUpdate(new UpdateInventoryRequest(requestConfig.getApiToken(), requestConfig.getProductName(), + requestConfig.getProductVersion(), projects, requestConfig.getUserKey(), requestConfig.getScanComment())); + if (senderConfig.isSendLogsToWss()) { + updateRequest.setLogData(getLogData()); + } + updateRequest.setRequesterEmail(requestConfig.getRequesterEmail()); + if (requestConfig.getProductToken() != null && !requestConfig.getProductToken().isEmpty()) { + updateRequest.setProductToken(requestConfig.getProductToken()); + } + try { + OfflineUpdateRequest offlineUpdateRequest = new OfflineUpdateRequest(updateRequest); + UpdateType updateTypeFinal; + // if the update type was forced by command or config -> set it + if (StringUtils.isNotBlank(senderConfig.getUpdateTypeValue())) { + try { + updateTypeFinal = UpdateType.valueOf(senderConfig.getUpdateTypeValue()); + } catch (Exception e) { + logger.info("Invalid value {} for updateType, defaulting to {}", senderConfig.getUpdateTypeValue(), UpdateType.OVERRIDE); + updateTypeFinal = UpdateType.OVERRIDE; + } + } else { + // Otherwise use the parameter in the file + updateTypeFinal = updateRequest.getUpdateType(); + } + logger.info("UpdateType offline set to {} ", updateTypeFinal); + updateRequest.setUpdateType(updateTypeFinal); + File outputDir = new File(offlineConfig.getWhiteSourceFolderPath()).getAbsoluteFile(); + if (!outputDir.exists() && !outputDir.mkdir()) { + throw new IOException("Unable to make output directory: " + outputDir); + } + File file = offlineUpdateRequest.generate(outputDir, offlineConfig.isZip(), offlineConfig.isPrettyJson()); + resultInfo = "Offline request generated successfully at " + file.getPath(); + logger.info(resultInfo); + } catch (IOException e) { + resultInfo = "Error generating offline update request: " + e.getMessage(); + logger.error(resultInfo); + } finally { + if (service != null) { + service.shutdown(); + } + } + return resultInfo; + } + + private String logResult(UpdateInventoryResult updateResult) { + StringBuilder resultLogMsg = new StringBuilder("Inventory update results for ").append(updateResult.getOrganization()).append(Constants.NEW_LINE); + logger.info("Inventory update results for {}", updateResult.getOrganization()); + // newly created projects + Collection createdProjects = updateResult.getCreatedProjects(); + if (createdProjects.isEmpty()) { + logger.info("No new projects found."); + resultLogMsg.append("No new projects found.").append(Constants.NEW_LINE); + } else { + logger.info("Newly created projects:"); + resultLogMsg.append("Newly created projects:").append(Constants.NEW_LINE); + for (String projectName : createdProjects) { + logger.info("# {}", projectName); + resultLogMsg.append(projectName).append(Constants.NEW_LINE); + } + } + // updated projects + Collection updatedProjects = updateResult.getUpdatedProjects(); + if (updatedProjects.isEmpty()) { + logger.info("No projects were updated."); + resultLogMsg.append("No projects were updated.").append(Constants.NEW_LINE); + } else { + logger.info("Updated projects:"); + resultLogMsg.append("Updated projects:").append(Constants.NEW_LINE); + for (String projectName : updatedProjects) { + logger.info("# {}", projectName); + resultLogMsg.append(projectName).append(Constants.NEW_LINE); + } + } + // reading projects' URLs + HashMap projectsUrls = updateResult.getProjectNamesToIds(); + if (projectsUrls != null && !projectsUrls.isEmpty()) { + for (String projectName : projectsUrls.keySet()) { + String appUrl = senderConfig.getServiceUrl().replace("agent", Constants.EMPTY_STRING); + String projectsUrl = appUrl + PROJECT_URL_PREFIX + projectsUrls.get(projectName); + logger.info("Project name: {}, URL: {}", projectName, projectsUrl); + resultLogMsg.append(Constants.NEW_LINE).append("Project name: ").append(projectName).append(", project URL:").append(projectsUrl); + } + } + // support token + String requestToken = updateResult.getRequestToken(); + if (StringUtils.isNotBlank(requestToken)) { + logger.info("Support Token: {}", requestToken); + resultLogMsg.append(Constants.NEW_LINE).append("Support Token: ").append(requestToken); + } + return resultLogMsg.toString(); + } + + private String getLogData() { + String logs = Constants.EMPTY_STRING; + ch.qos.logback.classic.Logger setLog = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger(Constants.MAP_LOG_NAME); + ConcurrentSkipListMap collectToSet = ((LogMapAppender) setLog.getAppender(Constants.MAP_APPENDER_NAME)).getLogEvents(); + // going over all the collected events, filtering out the empty ones, and writing them to a long string + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_FORMAT); + List events = collectToSet.values().stream().filter(iLoggingEvent -> !iLoggingEvent.getMessage().isEmpty() && + !iLoggingEvent.getMessage().equals(Constants.NEW_LINE)).collect(Collectors.toList()); + if (events.size() > MAX_LOG_EVENTS) { + events = events.stream().filter(iLoggingEvent -> iLoggingEvent.getLevel().levelInt >= Level.INFO.levelInt).collect(Collectors.toList()); + } + for (ILoggingEvent event : events) { + logs = logs.concat("[" + event.getLevel() + "] " + simpleDateFormat.format(new Date(event.getTimeStamp())) + + " - " + event.getFormattedMessage()).concat(Constants.NEW_LINE); + } + return logs; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/SingleFileScanner.java b/src/main/java/org/whitesource/agent/SingleFileScanner.java new file mode 100644 index 0000000..e243e52 --- /dev/null +++ b/src/main/java/org/whitesource/agent/SingleFileScanner.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent; + +import org.apache.tools.ant.DirectoryScanner; + +import java.io.File; + +/** + * A {@link org.apache.tools.ant.DirectoryScanner} for a single file. + * + * @author tom.shapira + */ +public class SingleFileScanner extends DirectoryScanner { + + /** + * Exposes the {@link DirectoryScanner#isIncluded(String)} method to check if a single file should be included + * in the scan. + * + * @param file for scanning + * @return weather the file should be included or not + */ + public boolean isIncluded(File file) { + return isIncluded(file.getAbsolutePath()); + } +} diff --git a/src/main/java/org/whitesource/agent/TempFolders.java b/src/main/java/org/whitesource/agent/TempFolders.java new file mode 100644 index 0000000..faa0705 --- /dev/null +++ b/src/main/java/org/whitesource/agent/TempFolders.java @@ -0,0 +1,73 @@ +package org.whitesource.agent; + +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.UniqueNamesGenerator; + +import java.io.File; +import java.nio.file.Paths; + + +public class TempFolders { + + /* --- Static members --- */ + + private static final String PATH_TO_TEMP_DIR = System.getProperty("java.io.tmpdir"); + private static final String WHITESOURCE_ARCHIVE_EXTRACTOR = "WhiteSource-ArchiveExtractor"; + private static final String WHITE_BUILD_GRADLE_FOLDER = "WhiteSource-Build-Gradle"; + private static final String WHITESOURCE_HTML_RESOLVER = "WhiteSource-html-resolver"; + private static final String WHITESOURCE_DOTNET_RESOLVER = "WhiteSource-DotnetRestore"; + private static final String WHITESOURCE_DOCKER = "WhiteSource-Docker"; + private static final String WHITESOURCE_SCM_CONNECTOR_TMP_DIRECTORY = "WhiteSource-ScmConnector"; + private static final String WHITESOURCE_PLATFORM_DEPENDENT_TMP_DIR = "WhiteSource-PlatformDependentFiles"; + private static final String WHITESOURCE_PYTHON_TEMP_FOLDER = "Whitesource_python_resolver"; + + public static final String UNIQUE_HTML_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITESOURCE_HTML_RESOLVER, Constants.EMPTY_STRING); + public static final String UNIQUE_GRADLE_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITE_BUILD_GRADLE_FOLDER, Constants.EMPTY_STRING); + public static final String UNIQUE_DOTNET_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITESOURCE_DOTNET_RESOLVER, Constants.EMPTY_STRING); + public static final String UNIQUE_PYTHON_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITESOURCE_PYTHON_TEMP_FOLDER, Constants.EMPTY_STRING); + public static final String UNIQUE_DOCKER_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITESOURCE_DOCKER, Constants.EMPTY_STRING); + public static final String UNIQUE_SCM_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITESOURCE_SCM_CONNECTOR_TMP_DIRECTORY, Constants.EMPTY_STRING); + public static final String UNIQUE_PLATFORM_DEPENDENT_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITESOURCE_PLATFORM_DEPENDENT_TMP_DIR, Constants.EMPTY_STRING); + public static final String UNIQUE_WHITESOURCE_ARCHIVE_EXTRACTOR_TEMP_FOLDER = UniqueNamesGenerator.createUniqueName(WHITESOURCE_ARCHIVE_EXTRACTOR, Constants.EMPTY_STRING); + + private static final String PATH_TO_UNIQUE_HTML_TEMP_FOLDER = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_HTML_TEMP_FOLDER).toString(); + private static final String PATH_TO_UNIQUE_GRADLE_TEMP_FOLDER = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_GRADLE_TEMP_FOLDER).toString(); + private static final String PATH_TO_UNIQUE_DOTNET_TEMP_FOLDER = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_DOTNET_TEMP_FOLDER).toString(); + private static final String PATH_TO_UNIQUE_PYTHON_TEMP_FOLDER = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_PYTHON_TEMP_FOLDER).toString(); + private static final String PATH_TO_UNIQUE_DOCKER_TEMP_FOLDER = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_DOCKER_TEMP_FOLDER).toString(); + private static final String PATH_TO_SCM_CONNECTOR_TMP_DIRECTORY = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_SCM_TEMP_FOLDER).toString(); + private static final String PATH_TO_UNIQUE_ARCHIVE_EXTRACTOR_TEMP_FOLDER = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_WHITESOURCE_ARCHIVE_EXTRACTOR_TEMP_FOLDER).toString(); + + // Agents api temp folder - CheckSumUtils folder :: calculateOtherPlatformSha1 method + private static final String PATH_TO_PLATFORM_DEPENDENT_TMP_DIR = Paths.get(PATH_TO_TEMP_DIR, UNIQUE_PLATFORM_DEPENDENT_TEMP_FOLDER).toString(); + + /* --- Constructors --- */ + + public TempFolders() { + + } + + /* --- Methods --- */ + + public void deleteTempFolders() { + deleteTempFoldersHelper(PATH_TO_UNIQUE_HTML_TEMP_FOLDER); + deleteTempFoldersHelper(PATH_TO_UNIQUE_GRADLE_TEMP_FOLDER); + deleteTempFoldersHelper(PATH_TO_UNIQUE_DOTNET_TEMP_FOLDER); + deleteTempFoldersHelper(PATH_TO_UNIQUE_PYTHON_TEMP_FOLDER); + deleteTempFoldersHelper(PATH_TO_UNIQUE_DOCKER_TEMP_FOLDER); + deleteTempFoldersHelper(PATH_TO_SCM_CONNECTOR_TMP_DIRECTORY); + deleteTempFoldersHelper(PATH_TO_PLATFORM_DEPENDENT_TMP_DIR); + deleteTempFoldersHelper(PATH_TO_UNIQUE_ARCHIVE_EXTRACTOR_TEMP_FOLDER); + } + + public void deleteTempFoldersHelper(String path) { + if (path != null) { + File file = new File(path); + if (file != null) { + FilesUtils.deleteDirectory(file); + } + } + } + + +} diff --git a/src/main/java/org/whitesource/agent/ViaComponents.java b/src/main/java/org/whitesource/agent/ViaComponents.java new file mode 100644 index 0000000..4508a99 --- /dev/null +++ b/src/main/java/org/whitesource/agent/ViaComponents.java @@ -0,0 +1,37 @@ +package org.whitesource.agent; + +import org.whitesource.agent.api.model.DependencyInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author raz.nitzan + */ +public class ViaComponents { + + /* --- Members --- */ + + private String appPath; + private ViaLanguage language; + private List dependencies = new ArrayList<>(); + + /* --- Constructor --- */ + + public ViaComponents(String appPath, ViaLanguage language) { + this.appPath = appPath; + this.language = language; + } + + public String getAppPath() { + return this.appPath; + } + + public ViaLanguage getLanguage() { + return this.language; + } + + public List getDependencies() { + return dependencies; + } +} diff --git a/src/main/java/org/whitesource/agent/ViaLanguage.java b/src/main/java/org/whitesource/agent/ViaLanguage.java new file mode 100644 index 0000000..bec1357 --- /dev/null +++ b/src/main/java/org/whitesource/agent/ViaLanguage.java @@ -0,0 +1,19 @@ +package org.whitesource.agent; + +/** + * @author raz.nitzan + */ +public enum ViaLanguage { + JAVA("java"), + JAVA_SCRIPT("javascript"); + + private final String language; + + ViaLanguage(String language) { + this.language = language; + } + + public String toString() { + return this.language; + } +} diff --git a/src/main/java/org/whitesource/agent/archive/ArchiveExtractor.java b/src/main/java/org/whitesource/agent/archive/ArchiveExtractor.java new file mode 100644 index 0000000..f519d31 --- /dev/null +++ b/src/main/java/org/whitesource/agent/archive/ArchiveExtractor.java @@ -0,0 +1,600 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.archive; + +import com.github.junrar.testutil.ExtractArchive; +import net.lingala.zip4j.core.ZipFile; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.model.FileHeader; +import org.apache.commons.compress.archivers.cpio.CpioArchiveEntry; +import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream; +import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.codehaus.plexus.archiver.tar.TarBZip2UnArchiver; +import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver; +import org.codehaus.plexus.archiver.tar.TarUnArchiver; +import org.codehaus.plexus.archiver.xz.XZUnArchiver; +import org.codehaus.plexus.logging.console.ConsoleLogger; +import org.redline_rpm.ReadableChannelWrapper; +import org.redline_rpm.Util; +import org.redline_rpm.header.AbstractHeader; +import org.redline_rpm.header.Format; +import org.redline_rpm.header.Header; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.utils.FilesScanner; +import org.whitesource.agent.utils.Pair; +import org.whitesource.agent.TempFolders; + +import java.io.*; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.FileSystems; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author anna.rozin + */ +public class ArchiveExtractor { + public static final String LAYER_TAR = "**/*layer.tar"; + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(ArchiveExtractor.class); + public static final int LONG_BOUND = 100000; + public static final String DEPTH = "_depth_"; + public static final String DEPTH_REGEX = DEPTH + "[0-9]"; + public static final String GLOB_PREFIX = "glob:"; + public static final String NULL_HEADER = "mainheader is null"; + + private final String JAVA_TEMP_DIR = System.getProperty("java.io.tmpdir"); + + + public static final List ZIP_EXTENSIONS = Arrays.asList("jar", "war", "aar", "ear", "egg", "zip", "whl", "sca", "sda", "nupkg"); + public static final List GEM_EXTENSIONS = Collections.singletonList("gem"); + public static final List TAR_EXTENSIONS = Arrays.asList("tar.gz", "tar", "tgz", "tar.bz2", "tar.xz", "xz"); + public static final List RPM_EXTENSIONS = Collections.singletonList("rpm"); + public static final List RAR_EXTENSIONS = Collections.singletonList("rar"); + + public static final String ZIP_EXTENSION_PATTERN; + public static final String GEM_EXTENSION_PATTERN; + public static final String TAR_EXTENSION_PATTERN; + public static final String RPM_EXTENSION_PATTERN; + public static final String RAR_EXTENSION_PATTERN; + public static final String RUBY_DATA_FILE = "data.tar.gz"; + public static final String TAR_SUFFIX = ".tar"; + public static final String GZ_SUFFIX = ".gz"; + public static final String BZ_SUFFIX = ".bz2"; + public static final String XZ_SUFFIX = ".xz"; + public static final String LZMA = "lzma"; + public static final String CPIO = ".cpio"; + public static final String TGZ_SUFFIX = ".tgz"; + + public static final String TAR_GZ_SUFFIX = TAR_SUFFIX + GZ_SUFFIX; + public static final String TAR_BZ2_SUFFIX = TAR_SUFFIX + BZ_SUFFIX; + + public static final String UN_ARCHIVER_LOGGER = "unArchiverLogger"; + public static final String GLOB_PATTERN_PREFIX = Constants.PATTERN + Constants.DOT; + public static final String PATTERN_PREFIX = ".*\\."; + public static final String XZ_UN_ARCHIVER_FILE_NAME = "compressedFile.tar"; + + static { + ZIP_EXTENSION_PATTERN = initializePattern(ZIP_EXTENSIONS); + GEM_EXTENSION_PATTERN = initializePattern(GEM_EXTENSIONS); + TAR_EXTENSION_PATTERN = initializePattern(TAR_EXTENSIONS); + RPM_EXTENSION_PATTERN = initializePattern(RPM_EXTENSIONS); + RAR_EXTENSION_PATTERN = initializePattern(RAR_EXTENSIONS); + } + + private static String initializePattern(List archiveExtensions) { + StringBuilder sb = new StringBuilder(); + for (String archiveExtension : archiveExtensions) { + sb.append(PATTERN_PREFIX); + sb.append(archiveExtension); + sb.append(Constants.PIPE); + } + return sb.toString().substring(0, sb.toString().lastIndexOf(Constants.PIPE)); + } + + /* --- Private members --- */ + + private final String[] archiveIncludesPattern; + private final String[] archiveExcludesPattern; + private final String[] filesExcludes; + private String randomString; + private String tempFolderNoDepth; + private boolean fastUnpack = false; + + /* --- Constructors --- */ + + public ArchiveExtractor(String[] archiveIncludes, String[] archiveExcludes, String[] filesExcludes, boolean fastUnpack) { + this(archiveIncludes, archiveExcludes, filesExcludes); + this.fastUnpack = fastUnpack; + } + + public ArchiveExtractor(String[] archiveIncludes, String[] archiveExcludes, String[] filesExcludes) { + if (archiveIncludes.length > 0 && StringUtils.isNotBlank(archiveIncludes[0])) { + this.archiveIncludesPattern = archiveIncludes; + } else { + // create ARCHIVE_EXTENSIONS only if archiveIncludes is empty + this.archiveIncludesPattern = createArchivesArray(); + } + this.archiveExcludesPattern = archiveExcludes; + this.filesExcludes = filesExcludes; + } + + private String getTempFolder(String scannerBaseDir) { + String creationDate = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + String tempFolder = JAVA_TEMP_DIR.endsWith(File.separator) ? JAVA_TEMP_DIR + TempFolders.UNIQUE_WHITESOURCE_ARCHIVE_EXTRACTOR_TEMP_FOLDER + File.separator + creationDate : + JAVA_TEMP_DIR + File.separator + TempFolders.UNIQUE_WHITESOURCE_ARCHIVE_EXTRACTOR_TEMP_FOLDER + File.separator + creationDate; + String destDirectory = tempFolder + Constants.UNDERSCORE + this.randomString; + int separatorIndex = scannerBaseDir.lastIndexOf(File.separator); + + if (separatorIndex != -1) { + destDirectory = destDirectory + scannerBaseDir.substring(separatorIndex, scannerBaseDir.length()); + try { + // this solves the tilda issue in filepath in windows (mangled Windows filenames) + destDirectory = new File(destDirectory).getCanonicalPath().toString(); + } catch (IOException e) { + logger.warn("Error getting the absolute file name ", e); + } + } + return destDirectory; + } + + /* --- Public methods --- */ + + /** + * The Method extracts all the Archive files according to the archiveExtractionDepth. + * archiveExtractionDepth defined by the user in the configuration file. + *

+ * The archiveExtractionDepth default value is 0 - no archive scanning, the max value is 3. + * By default the method scans jar/war/ear. + * If archiveIncludes/archiveExcludes params are defined the method will act accordingly. + * + * @param scannerBaseDir - directory for scanning. + * @param archiveExtractionDepth - drill down hierarchy level in archive files + * @param archiveDirectories list of directories + * @return the temp directory for the extracted files. + */ + public String extractArchives(String scannerBaseDir, int archiveExtractionDepth, List archiveDirectories) { + this.randomString = String.valueOf(ThreadLocalRandom.current().nextLong(0, LONG_BOUND)); + this.tempFolderNoDepth = getTempFolder(scannerBaseDir); + logger.debug("Base directory is {}, extraction depth is set to {}", scannerBaseDir, archiveExtractionDepth); + Map> allFiles = new HashMap<>(); + // Extract again if needed according archiveExtractionDepth parameter + for (int curLevel = 0; curLevel < archiveExtractionDepth; curLevel++) { + String folderToScan; + String folderToExtract; + if (curLevel == 0) { + folderToScan = scannerBaseDir; + } else { + folderToScan = getDepthFolder(curLevel - 1); + } + folderToExtract = getDepthFolder(curLevel); + Pair retrieveFilesWithFolder = getSearchedFileNames(folderToScan); + if (retrieveFilesWithFolder == null || retrieveFilesWithFolder.getKey().length <= 0) { + break; + } else { + String[] fileNames = retrieveFilesWithFolder.getKey(); + folderToScan = retrieveFilesWithFolder.getValue(); + + Pair> filesFound = new Pair<>(folderToScan, Arrays.stream(fileNames).collect(Collectors.toList())); + Map foundFiles; + if (fastUnpack) { + foundFiles = handleArchiveFilesFast(folderToExtract, filesFound); + } else { + foundFiles = handleArchiveFiles(folderToExtract, filesFound); + } + allFiles.put(String.valueOf(curLevel), foundFiles); + } + } + if (!allFiles.isEmpty()) { + String parentDirectory = new File(this.tempFolderNoDepth).getParent(); + archiveDirectories.add(parentDirectory); + return parentDirectory; + } else { + // if unable to extract, return null + return null; + } + } + + // extract image layers + public void extractDockerImageLayers(File imageTarFile, File imageExtractionDir, Boolean deleteTarFiles) { + FilesScanner filesScanner = new FilesScanner(); + boolean success = false; + // docker layers are saved as TAR file (we save it as TAR) + if (imageTarFile.getName().endsWith(TAR_SUFFIX)) { + success = unTar(imageTarFile.getName().toLowerCase(), imageExtractionDir.getAbsolutePath(), imageTarFile.getPath()); + boolean deleted = false; + if (deleteTarFiles) { + deleted = imageTarFile.delete(); + } + if (!deleted) { + logger.warn("Was not able to delete {} (docker image TAR file)", imageTarFile.getName()); + } + } + if (success) { + String[] fileNames = filesScanner.getDirectoryContent(imageExtractionDir.getAbsolutePath(), new String[]{LAYER_TAR}, new String[]{}, true, false); + for (String filename : fileNames) { + File layerToExtract = new File(imageExtractionDir + File.separator + filename); + extractDockerImageLayers(layerToExtract, layerToExtract.getParentFile(), deleteTarFiles); + } + } else { + logger.warn("Was not able to extract {} (docker image TAR file)", imageTarFile.getName()); + } + } + + private String getDepthFolder(int depth) { + return this.tempFolderNoDepth + DEPTH + depth; + } + + /* --- Private methods --- */ + + private String[] createArchivesArray() { + Collection archiveExtensions = new ArrayList<>(); + archiveExtensions.addAll(ZIP_EXTENSIONS); + archiveExtensions.addAll(GEM_EXTENSIONS); + archiveExtensions.addAll(TAR_EXTENSIONS); + + String[] archiveIncludesPattern = new String[archiveExtensions.size()]; + int i = 0; + for (String extension : archiveExtensions) { + archiveIncludesPattern[i++] = GLOB_PATTERN_PREFIX + extension; + } + return archiveIncludesPattern; + } + + private Pair getSearchedFileNames(String fileOrFolderToScan) { + String[] foundFiles = null; + File file = new File(fileOrFolderToScan); + + String folderToScan; + if (file.exists()) { + FilesScanner filesScanner = new FilesScanner(); + if (file.isDirectory()) { + // scan directory + foundFiles = filesScanner.getDirectoryContent(fileOrFolderToScan, archiveIncludesPattern, archiveExcludesPattern, false, false); + folderToScan = fileOrFolderToScan; + return new Pair<>(foundFiles, folderToScan); + } else { + //// handle file passed in -d parameter + //// check if file matches archive GLOB patterns + boolean included = filesScanner.isIncluded(file, archiveIncludesPattern, archiveExcludesPattern, false, false); + if (included) { + folderToScan = file.getParent(); + String relativeFilePath = new File(folderToScan).toURI().relativize(new File(file.getAbsolutePath()).toURI()).getPath(); + foundFiles = new String[]{relativeFilePath}; + return new Pair<>(foundFiles, folderToScan); + } + } + filesScanner = null; + } + return null; + } + + private Map handleArchiveFiles(String baseFolderToExtract, Pair> fileNames) { + Map founded = new HashMap<>(); + for (String fileName : fileNames.getValue()) { + String archivePath = Paths.get(fileNames.getKey(), fileName).toString(); + String unpackFolder = Paths.get(baseFolderToExtract, FilenameUtils.removeExtension(fileName)).toString(); + Pair dataToUnpack = new Pair<>(archivePath, unpackFolder); + Pair foundArchive = getUnpackedResult(dataToUnpack); + if (foundArchive != null) { + founded.put(foundArchive.getKey(), foundArchive.getValue()); + } + } + return founded; + } + + private Map handleArchiveFilesFast(String baseFolderToExtract, Pair> fileNames) { + Collection dataToUnpack = fileNames.getValue().stream().map(fileName -> { + String archivePath = Paths.get(fileNames.getKey(), fileName).toString(); + String unpackFolder = Paths.get(baseFolderToExtract, FilenameUtils.removeExtension(fileName)).toString(); + return new Pair(archivePath, unpackFolder); + }).collect(Collectors.toList()); + return processCollections(dataToUnpack); + } + + public Map processCollections(Collection unitsOfWork) { + int numberOfThreads = Runtime.getRuntime().availableProcessors(); + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + List> handles = new ArrayList<>(); + + List> callableList = new ArrayList<>(); + unitsOfWork.stream().forEach(unitOfWork -> callableList.add(() -> getUnpackedResult(unitOfWork))); + + for (Callable callable : callableList) { + Future handle = executorService.submit(callable); + handles.add(handle); + } + + Map results = new HashMap<>(); + for (Future h : handles) { + try { + Pair dataToUnpack = h.get(); + results.put(dataToUnpack.getKey(), dataToUnpack.getValue()); + } catch (InterruptedException e) { + logger.warn("Error: {}", e.getMessage()); + } catch (ExecutionException e) { + logger.warn("Error: {}", e.getMessage()); + } + } + + executorService.shutdownNow(); + return results; + } + + private Pair getUnpackedResult(Pair dataToUnpack) { + boolean foundArchive = false; + String innerDir = dataToUnpack.getValue(); + String fileKey = dataToUnpack.getKey(); + String lowerCaseFileName = fileKey.toLowerCase(); + + if (lowerCaseFileName.matches(ZIP_EXTENSION_PATTERN)) { + foundArchive = unZip(innerDir, fileKey); + } else if (lowerCaseFileName.matches(GEM_EXTENSION_PATTERN)) { + foundArchive = unTar(lowerCaseFileName, innerDir, fileKey); + innerDir = innerDir + File.separator + RUBY_DATA_FILE; + foundArchive = unTar(RUBY_DATA_FILE, innerDir + this.randomString, innerDir); + innerDir = innerDir + this.randomString; + } else if (lowerCaseFileName.matches(TAR_EXTENSION_PATTERN)) { + foundArchive = unTar(lowerCaseFileName, innerDir, fileKey); + // innerDir = innerDir.replaceAll(TAR_SUFFIX, BLANK); + } else if (lowerCaseFileName.matches(RPM_EXTENSION_PATTERN)) { + foundArchive = handleRpmFile(innerDir, fileKey); + } else if (lowerCaseFileName.matches(RAR_EXTENSION_PATTERN)) { + foundArchive = extractRarFile(innerDir, fileKey); + } else { + logger.warn("Error: {} is unsupported archive type", fileKey); + } + if (foundArchive) { + Pair resultArchive = new Pair(lowerCaseFileName, innerDir); + return resultArchive; + } else + return null; + } + + private boolean extractRarFile(String innerDir, String fileKey) { + boolean foundArchive; + File destDir = new File(innerDir); + if (!destDir.exists()) { + destDir.mkdirs(); + } + try { + ExtractArchive.extractArchive(fileKey, innerDir); + foundArchive = true; + } catch (Exception e) { + logger.warn("Error extracting file {}: {}", fileKey, e.getMessage()); + try { + //if the header is missing try to extract the rar file with zip extension - WSE-450 + if (e.getMessage().contains(NULL_HEADER) && new ZipFile(fileKey) instanceof ZipFile) { + logger.info("Retrying extraction {}", fileKey); + foundArchive = unZip(innerDir, fileKey); + } + } catch (ZipException e1) { + logger.warn("Error extracting file {}: {}", fileKey, e.getMessage()); + foundArchive = false; + } + } + return true; + } + + // Open and extract data from zip pattern files + private boolean unZip(String innerDir, String archiveFile) { + boolean success = true; + ZipFile zipFile; + try { + zipFile = new ZipFile(archiveFile); + // Get the list of file headers from the zip file before unpacking + List fileHeaderList = zipFile.getFileHeaders(); + + List matchers = Arrays.stream(filesExcludes).map(fileExclude -> + FileSystems.getDefault().getPathMatcher(GLOB_PREFIX + fileExclude)).collect(Collectors.toList()); + // Loop through the file headers and extract only files that are not matched by fileExcludes patterns + for (int i = 0; i < fileHeaderList.size(); i++) { + FileHeader fileHeader = (FileHeader) fileHeaderList.get(i); + String fileName = fileHeader.getFileName(); + if (filesExcludes.length > 0) { + Predicate matchesExcludes = pathMatcher -> pathMatcher.matches(Paths.get(innerDir, fileName)); + if (matchers.stream().noneMatch(matchesExcludes)) { + zipFile.extractFile(fileHeader, innerDir); + } + } else { + zipFile.extractFile(fileHeader, innerDir); + } + } + } catch (Exception e) { + success = false; + logger.warn("Error extracting file {}: {}", archiveFile, e.getMessage()); + logger.debug("Error extracting file {}: {}", archiveFile, e.getStackTrace()); + } finally { + // remove reference to zip file + zipFile = null; + } + return success; + } + + // Open and extract data from Tar pattern files + private boolean unTar(String fileName, String innerDir, String archiveFile) { + boolean success = true; + TarUnArchiver unArchiver = new TarUnArchiver(); + try { + File destDir = new File(innerDir); + if (!destDir.exists()) { + destDir.mkdirs(); + } + if (fileName.endsWith(TAR_GZ_SUFFIX) || fileName.endsWith(TGZ_SUFFIX)) { + unArchiver = new TarGZipUnArchiver(); + } else if (fileName.endsWith(TAR_BZ2_SUFFIX)) { + unArchiver = new TarBZip2UnArchiver(); + } else if (fileName.endsWith(XZ_SUFFIX)) { + String destFileUrl = destDir.getCanonicalPath() + Constants.BACK_SLASH + XZ_UN_ARCHIVER_FILE_NAME; + success = unXz(new File(archiveFile), destFileUrl); + archiveFile = destFileUrl; + } + if (success) { + unArchiver.enableLogging(new ConsoleLogger(ConsoleLogger.LEVEL_DISABLED, UN_ARCHIVER_LOGGER)); + unArchiver.setSourceFile(new File(archiveFile)); + unArchiver.setDestDirectory(destDir); + unArchiver.extract(); + } + } catch (Exception e) { + success = false; + logger.warn("Error extracting file {}: {}", fileName, e.getMessage()); + } + return success; + } + + // extract xz files + public boolean unXz(File srcFileToArchive, String destFilePath) { + boolean success = true; + try { + XZUnArchiver XZUnArchiver = new XZUnArchiver(); + XZUnArchiver.enableLogging(new ConsoleLogger(ConsoleLogger.LEVEL_DISABLED, UN_ARCHIVER_LOGGER)); + XZUnArchiver.setSourceFile(srcFileToArchive); + XZUnArchiver.setDestFile(new File(destFilePath)); + XZUnArchiver.extract(); + } catch (Exception e) { + success = false; + logger.warn("Failed to extract Xz file : {} - {}", srcFileToArchive.getPath(), e.getMessage()); + } + return success; + } + + // Open and extract data from rpm files + private boolean handleRpmFile(String innerDir, String archiveFile) { + boolean success = true; + File rpmFile = new File(archiveFile); + FileInputStream rpmFIS = null; + try { + rpmFIS = new FileInputStream(rpmFile.getPath()); + } catch (FileNotFoundException e) { + success = false; + logger.warn("File not found: {}", archiveFile); + } + + Format format = null; + ReadableByteChannel channel = Channels.newChannel(rpmFIS); + ReadableChannelWrapper channelWrapper = new ReadableChannelWrapper(channel); + try { + format = new org.redline_rpm.Scanner().run(channelWrapper); + } catch (IOException e) { + success = false; + logger.warn("Error reading RPM file {}: {}", archiveFile, e.getCause()); + } + + if (format != null) { + Header header = format.getHeader(); + FileOutputStream cpioOS = null; + FileOutputStream cpioEntryOutputStream = null; + CpioArchiveInputStream cpioIn = null; + File cpioFile = null; + try { + // extract all .cpio file + // get input stream according to payload compressor type + InputStream inputStream; + AbstractHeader.Entry pcEntry = header.getEntry(Header.HeaderTag.PAYLOADCOMPRESSOR); + String[] pc = (String[]) pcEntry.getValues(); + if (pc[0].equals(LZMA)) { + try { + inputStream = new LZMACompressorInputStream(rpmFIS); + } catch (Exception e) { + throw new IOException("Failed to load LZMA compression stream", e); + } + } else { + inputStream = Util.openPayloadStream(header, rpmFIS); + } + + cpioFile = new File(rpmFile.getPath() + CPIO); + cpioOS = new FileOutputStream(cpioFile); + IOUtils.copy(inputStream, cpioOS); + // extract files from .cpio + File extractDestination = new File(innerDir); + extractDestination.mkdirs(); + cpioIn = new CpioArchiveInputStream(new FileInputStream(cpioFile)); + + CpioArchiveEntry cpioEntry; + while ((cpioEntry = (CpioArchiveEntry) cpioIn.getNextEntry()) != null) { + String entryName = cpioEntry.getName(); + String lowercaseName = entryName.toLowerCase(); + File file = new File(extractDestination, getFileName(entryName)); + cpioEntryOutputStream = new FileOutputStream(file); + IOUtils.copy(cpioIn, cpioEntryOutputStream); + String innerExtractionDir; + if (lowercaseName.matches(TAR_EXTENSION_PATTERN)) { + innerExtractionDir = innerDir + File.separator + entryName + this.randomString; + unTar(file.getName(), innerExtractionDir, file.getPath()); + } else if (lowercaseName.matches(ZIP_EXTENSION_PATTERN)) { + innerExtractionDir = innerDir + File.separator + entryName + this.randomString; + unZip(innerExtractionDir, file.getPath()); + } + // close + closeResource(cpioEntryOutputStream); + } + } catch (IOException e) { + logger.error("Error unpacking rpm file {}: {}", rpmFile.getName(), e.getMessage()); + } finally { + closeResource(cpioEntryOutputStream); + closeResource(cpioIn); + closeResource(cpioOS); + deleteFile(cpioFile); + } + } + return success; + } + + private void deleteFile(File cpioFile) { + try { + FileUtils.forceDelete(cpioFile); + } catch (IOException e) { + logger.warn("Error deleting cpio file {}: {}", cpioFile.getName(), e.getMessage()); + } + } + + private void closeResource(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } catch (IOException e) { + logger.warn("Error closing file {}: {}", resource.toString(), e.getMessage()); + } + } + } + + // parse name without directories + private String getFileName(String name) { + //check if the environment is linux or windows + if (name.contains(Constants.FORWARD_SLASH)) { + name = name.substring(name.lastIndexOf(Constants.FORWARD_SLASH) + 1, name.length()); + } else if (name.contains(Constants.BACK_SLASH)) { + name = name.substring(name.lastIndexOf(Constants.BACK_SLASH) + 1, name.length()); + } + return name; + } + + +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/AbstractDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/AbstractDependencyResolver.java new file mode 100644 index 0000000..04407c0 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/AbstractDependencyResolver.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +import org.apache.commons.lang.StringUtils; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyType; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.*; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author eugen.horovitz + */ +public abstract class AbstractDependencyResolver { + + /* --- Static Members --- */ + + protected static final String GLOB_PATTERN = "**/"; + protected static final String fileSeparator = System.getProperty(Constants.FILE_SEPARATOR); + protected IBomParser bomParser; + + /* --- Abstract methods --- */ + + protected abstract ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) throws FileNotFoundException; + + protected abstract Collection getExcludes(); + + protected abstract DependencyType getDependencyType(); + + protected abstract String getDependencyTypeName(); + + protected abstract String[] getBomPattern(); + + public abstract Collection getManifestFiles(); + + protected abstract Collection getLanguageExcludes(); + + protected Collection getRelevantScannedFolders(Collection scannedFolders) { + if (scannedFolders == null) { + return new HashSet<>(); + } + if (scannedFolders.isEmpty()) { + return scannedFolders; + } + Collection foldersToRemove = new HashSet<>(); + for(String folder : scannedFolders) { + for (String subFolder : scannedFolders) { + if (subFolder.contains(folder) && !subFolder.equals(folder)) { + foldersToRemove.add(subFolder); + } + } + } + scannedFolders.removeAll(foldersToRemove); + return scannedFolders; + } + + protected boolean printResolvedFolder() { + return true; + } + + public abstract Collection getSourceFileExtensions(); + + /* --- Protected methods --- */ + protected List extensionPattern(List extensions) { + List extensionsPatternStr = new LinkedList<>(); + for (String extension : extensions) { + extensionsPatternStr.add(Constants.PATTERN + extension); + } + return extensionsPatternStr; + } + protected List normalizeLocalPath(String parentFolder, String topFolderFound, Collection excludes, String folderToIgnore) { + String normalizedRoot = new File(parentFolder).getPath(); + if (normalizedRoot.equals(topFolderFound)) { + topFolderFound = topFolderFound + .replace(normalizedRoot, Constants.EMPTY_STRING) + .replace(Constants.BACK_SLASH, Constants.FORWARD_SLASH); + } else { + topFolderFound = topFolderFound + .replace(parentFolder, Constants.EMPTY_STRING) + .replace(Constants.BACK_SLASH, Constants.FORWARD_SLASH); + } + + if (topFolderFound.length() > 0) + topFolderFound = topFolderFound.substring(1, topFolderFound.length()) + Constants.FORWARD_SLASH; + + String finalRes = topFolderFound; + if (StringUtils.isBlank(folderToIgnore)) { + return excludes.stream().map(exclude -> finalRes + exclude).collect(Collectors.toList()); + } else { + return excludes.stream().map(exclude -> finalRes + folderToIgnore + Constants.FORWARD_SLASH + exclude).collect(Collectors.toList()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/BomFile.java b/src/main/java/org/whitesource/agent/dependency/resolver/BomFile.java new file mode 100644 index 0000000..b57d25f --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/BomFile.java @@ -0,0 +1,172 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.dependency.resolver.npm.RegistryType; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; + +import java.util.Map; + +/** + * @author eugen.horovitz + */ +public class BomFile { + + /* --- Members --- */ + + private final String name; + private final String version; + private String groupId; + private String sha1; + private String fileName; + private final String localFileName; + private final Map dependencies; + private Map optionalDependencies; + private String resolved; + private boolean scopedPackage; + private RegistryType registryType; + + public static String DUMMY_PARAMETER_SCOPE_PACKAGE = "{dummyParameterOfScopePackage}"; + private final Logger logger = LoggerFactory.getLogger(BomFile.class); + private static String NPM_REGISTRY = "registry.npmjs.org"; + private static final String NPM_REGISTRY1 = "npm/registry/"; + private static final String SCOPED_PACKAGE = "@"; + private static final String HTTPS = "https"; + private static final String HTTP = "http"; + + /* --- Constructors --- */ + + public BomFile(String name, String version, String sha1, String fileName, String localFileName, + Map dependencies, Map optionalDependencies, String resolved, RegistryType registryType) { + this.name = name; + this.version = version; + this.sha1 = sha1; + this.fileName = fileName; + this.localFileName = localFileName; + this.dependencies = dependencies; + this.optionalDependencies = optionalDependencies; + this.resolved = resolved; + this.scopedPackage = false; + this.groupId = null; + this.registryType = registryType; + } + + public BomFile(String groupId, String artifactId, String version, String bomPath) { + this(artifactId, version, null, null, bomPath, null, null, null, null); + this.groupId = groupId; + } + + /* --- Public method --- */ + + public boolean isValid() { + return StringUtils.isNoneBlank(name, version); + } + + /* --- Getters --- */ + + public String getName() { + return name; + } + + public String getLocalFileName() { + return localFileName; + } + + public String getVersion() { + return version; + } + + public String getSha1() { + return sha1; + } + + public String getFileName() { + return fileName; + } + + public String getGroupId() { + return groupId; + } + + public static String getUniqueDependencyName(String name, String version) { + return name + SCOPED_PACKAGE + version.replace("v", Constants.EMPTY_STRING); + } + + public String getUniqueDependencyName() { + return getUniqueDependencyName(name, version); + } + + public Map getDependencies() { + return dependencies; + } + + public Map getOptionalDependencies() { + return optionalDependencies; + } + + public RegistryType getRegistryType() { + return this.registryType; + } + + public String getRegistryPackageUrl() { + String registryPackageUrl = null; + if (StringUtils.isEmpty(this.resolved)) { + logger.debug("resolved url in file is empty"); + return StringUtils.EMPTY; + } + logger.debug("resolved url in file = " + this.resolved); + if (this.resolved.contains("git+") || this.resolved.contains("github:")) { + // temp solution for WSE-204 + logger.info("This configuration - " + this.name + " (remote repository packages) is not supported by WhiteSource. Please use direct URL package references."); + return StringUtils.EMPTY; + } + + if (this.registryType == RegistryType.ARTIFACTORY) { + // example: change this url: http://localhost:8081/artifactory/api/npm/npmExample/q-1.5.1.tgz to http://localhost:8081/artifactory/api/npm/npmExample/q-1.5.1.json + registryPackageUrl = this.resolved.substring(0, this.resolved.length() - 3); + registryPackageUrl = registryPackageUrl + "json"; + } else if (this.resolved.contains(SCOPED_PACKAGE) || this.registryType != RegistryType.NPM_REGISTRY) { + // resolve rare cases where the package's name is a sub-string of the registry's url + int npmRegistryIndex = this.resolved.indexOf(NPM_REGISTRY1); + registryPackageUrl = this.resolved.substring(0, this.resolved.indexOf(this.name, npmRegistryIndex) + this.name.length()); + int lastSlashIndex = registryPackageUrl.lastIndexOf('/'); + registryPackageUrl = registryPackageUrl.substring(0, lastSlashIndex) + DUMMY_PARAMETER_SCOPE_PACKAGE + registryPackageUrl.substring(lastSlashIndex + 1); + this.scopedPackage = true; + } else { + String urlName = Constants.FORWARD_SLASH + this.name + Constants.FORWARD_SLASH; + registryPackageUrl = this.resolved.substring(0, this.resolved.indexOf(urlName) + urlName.length()); + registryPackageUrl = registryPackageUrl + this.version; + if (registryPackageUrl.startsWith(HTTPS)) { + String urlWithoutHttps = registryPackageUrl.substring(HTTPS.length()); + registryPackageUrl = HTTP + urlWithoutHttps; + } + } + logger.debug("resolved url in link = " + registryPackageUrl); + return registryPackageUrl; + } + + public boolean isScopedPackage() { + return this.scopedPackage; + } + + @Override + public String toString() { + return String.join(Constants.DOT, getGroupId(), getName(), getVersion()); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/BomParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/BomParser.java new file mode 100644 index 0000000..1568ff5 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/BomParser.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author eugen.horovitz + */ +public abstract class BomParser implements IBomParser{ + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(BomParser.class); + + /* --- Public methods --- */ + + public BomFile parseBomFile(String bomPath) { + BomFile bomFile = null; + String json = null; + try (InputStream is = new FileInputStream(bomPath)) { + json = IOUtils.toString(is); + } catch (FileNotFoundException e) { + logger.error("file Not Found: {}", bomPath); + } catch (IOException e) { + logger.error("error getting file : {}", e.getMessage()); + } + + if (json != null) { + try { + bomFile = parseBomFile(json, bomPath); + } catch (Exception e) { + logger.debug("Invalid file {}", bomPath); + } + } + return bomFile; + } + + /* --- Abstract methods --- */ + + protected abstract BomFile parseBomFile(String jsonText, String localFileName); + + protected abstract String getFilename(String name, String version); +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyCollector.java new file mode 100644 index 0000000..6001213 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyCollector.java @@ -0,0 +1,159 @@ +package org.whitesource.agent.dependency.resolver.CocoaPods; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.hash.HashCalculator; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * @author raz.nitzan + */ +public class CocoaPodsDependencyCollector extends DependencyCollector { + + /* --- Static Members --- */ + + private static final String PODS = "PODS"; + private static final String DEPENDENCIES = "DEPENDENCIES"; + private static final String PATTERN_DIRECT_LINE = " -"; + private static final String PATTERN_TRANSITIVE_DEPENDENCY = " -"; + + /* --- Members --- */ + + private final Logger logger = LoggerFactory.getLogger(CocoaPodsDependencyCollector.class); + private HashCalculator hashCalculator = new HashCalculator(); + + /* --- public methods --- */ + + public Collection collectDependencies(String podFileLock) { + Collection dependencies = new LinkedList<>(); + List directDependenciesLines = new LinkedList<>(); + List allDependenciesLines = new LinkedList<>(); + if (getPodsAndDependenciesSection(directDependenciesLines, allDependenciesLines, podFileLock)) { + dependencies.addAll(parseDependenciesLines(directDependenciesLines, allDependenciesLines, podFileLock)); + } else { + logger.warn("Failed to parse the Podfile.lock in {}", podFileLock); + } + return getSingleProjectList(dependencies); + } + + /* --- private methods --- */ + + private boolean getPodsAndDependenciesSection(List directDependenciesLines, List allDependenciesLines, String podFileLock) { + boolean successReadPodfile = true; + boolean podsSection = false; + boolean dependenciesSection = false; + try (BufferedReader br = new BufferedReader(new FileReader(podFileLock))) { + String line; + logger.debug("The content of Podfile.lock - {}:", podFileLock); + while ((line = br.readLine()) != null) { + logger.debug(line); + if (line.startsWith(PODS)) { + dependenciesSection = false; + podsSection = true; + } else if (line.startsWith(DEPENDENCIES)) { + podsSection = false; + dependenciesSection = true; + } else if (line.trim().equals(Constants.EMPTY_STRING)) { + podsSection = false; + dependenciesSection = false; + } else if (podsSection) { + allDependenciesLines.add(line); + } else if (dependenciesSection) { + directDependenciesLines.add(line); + } + } + } catch (IOException e) { + logger.warn("Couldn't read the Podfile.lock: {}", podFileLock); + successReadPodfile = false; + } + return successReadPodfile; + } + + private Collection parseDependenciesLines(List directDependenciesLines, List allDependenciesLines, String podFileLock) { + Collection dependencies = new LinkedList<>(); + for (String lineDirect : directDependenciesLines) { + if (StringUtils.isNotEmpty(lineDirect)) { + int indexFirstBracket = lineDirect.indexOf(Constants.OPEN_BRACKET); + String prefixToSearch = lineDirect + Constants.WHITESPACE; + if (indexFirstBracket > -1) { + prefixToSearch = lineDirect.substring(0, indexFirstBracket); + } + DependencyInfo dependencyInfo = getDependencyInfo(allDependenciesLines, prefixToSearch, podFileLock); + if (dependencyInfo != null) { + dependencies.add(dependencyInfo); + } + } + } + return dependencies; + } + + private DependencyInfo getDependencyInfo(List allDependenciesLines, String prefixToSearch, String podFileLock) { + DependencyInfo dependency = null; + boolean getToRightDependency = false; + for (String line : allDependenciesLines) { + String fixed = line.replace(Constants.QUOTATION_MARK, Constants.EMPTY_STRING); + String fixedPrefix = prefixToSearch.replace(Constants.QUOTATION_MARK, Constants.EMPTY_STRING); + if (line.startsWith(PATTERN_DIRECT_LINE) && fixed.startsWith(fixedPrefix)) { + getToRightDependency = true; + dependency = createDependencyFromLine(line, podFileLock); + } else if (getToRightDependency && line.startsWith(PATTERN_DIRECT_LINE)) { + return dependency; + } else if (getToRightDependency && line.startsWith(PATTERN_TRANSITIVE_DEPENDENCY)) { + if (dependency != null) { + int indexFirstBracket = line.indexOf(Constants.OPEN_BRACKET); + String newPrefixToSearch = line.substring(2) + Constants.WHITESPACE; + if (indexFirstBracket > -1) { + newPrefixToSearch = line.substring(2, indexFirstBracket); + } + DependencyInfo childDependency = getDependencyInfo(allDependenciesLines, newPrefixToSearch, podFileLock); + if (childDependency != null) { + dependency.getChildren().add(childDependency); + } else { + logger.warn("could not get info for child dependency {}", newPrefixToSearch); + } + } + } + } + return dependency; + } + + private DependencyInfo createDependencyFromLine(String line, String podFileLock) { + int indexOpenBracket = line.indexOf(Constants.OPEN_BRACKET); + // get the name and version from line like this: - AFNetworking (2.2.1): + String name = line.substring(line.indexOf(Constants.DASH) + 2, indexOpenBracket - 1); + if (name.startsWith(Constants.QUOTATION_MARK)) { + name = name.substring(1); + } + String version = line.substring(indexOpenBracket + 1, line.lastIndexOf(Constants.CLOSE_BRACKET)); + DependencyInfo dependency = new DependencyInfo(); + dependency.setArtifactId(name); + dependency.setVersion(version); + dependency.setGroupId(name); + dependency.setFilename(name + Constants.DASH + version); + dependency.setDependencyFile(podFileLock); + dependency.setSystemPath(podFileLock); + String sha1 = null; + try { + sha1 = this.hashCalculator.calculateSha1ByNameVersionAndType(name, version, DependencyType.COCOAPODS); + } catch (IOException e) { + logger.debug("Failed to calculate sha1 of: {}", name); + } + if (sha1 != null) { + dependency.setSha1(sha1); + } + dependency.setDependencyType(DependencyType.COCOAPODS); + return dependency; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyResolver.java new file mode 100644 index 0000000..66693fc --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/CocoaPods/CocoaPodsDependencyResolver.java @@ -0,0 +1,142 @@ +package org.whitesource.agent.dependency.resolver.CocoaPods; + +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.utils.CommandLineProcess; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author raz.nitzan + */ +public class CocoaPodsDependencyResolver extends AbstractDependencyResolver { + + /* --- Static Members --- */ + + private static final String PODFILE = "Podfile"; + private static final String PODFILE_LOCK = "Podfile.lock"; + private static final String SWIFT_EXT = ".swift"; + private static final String H_EXT = ".h"; + private static final String M_EXT = ".m"; + private static final String POD = "pod"; + private static final String HPP_EXT = ".hpp"; + private static final String CPP_EXT = ".cpp"; + private static final String CC_EXT = ".cc"; + private static final String C_EXT = ".c"; + + /* --- Members --- */ + + private boolean runPreStep; + private boolean ignoreSourceFiles; + private Collection excludes = new ArrayList<>(); + private final Logger logger = LoggerFactory.getLogger(CocoaPodsDependencyResolver.class); + + /* --- Constructor --- */ + + public CocoaPodsDependencyResolver(boolean runPreStep, boolean ignoreSourceFiles) { + this.runPreStep = runPreStep; + this.ignoreSourceFiles = ignoreSourceFiles; + } + + @Override + public ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set podFiles) { + if (ignoreSourceFiles) { + addExcludesSourcefilesExtenstions(); + } + Collection dependencyInfos = new LinkedList<>(); + // go over all Pod files, generate Podfile.lock if doesn't exist(if runPreStep=true) and collect dependencies + for (String podFile : podFiles) { + logger.debug("Found Podfile: {}", podFile); + String parentFileOfPodFile = new File(podFile).getParent(); + String podFileLockString = new File(podFile).getParent() + File.separator + PODFILE_LOCK; + File podFileLock = new File(podFileLockString); + CocoaPodsDependencyCollector cocoaPodsDependencyCollector = new CocoaPodsDependencyCollector(); + if (podFileLock.exists()) { + Collection projects = cocoaPodsDependencyCollector.collectDependencies(podFileLockString); + dependencyInfos.addAll(projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList())); + } else if (this.runPreStep) { + boolean processFailed = executePodInstall(parentFileOfPodFile); + if (processFailed) { + logger.warn("Failed to run 'pod install' in folder: {}", parentFileOfPodFile); + } else { + Collection projects = cocoaPodsDependencyCollector.collectDependencies(podFileLockString); + dependencyInfos.addAll(projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList())); + } + } else { + logger.info("Found Podfile, Podfile.lock doesn't exist. Please run 'pod install' or set 'CocoaPods.runPreStep=true'."); + } + } + return new ResolutionResult(dependencyInfos, getExcludes(), getDependencyType(), topLevelFolder); + } + + private void addExcludesSourcefilesExtenstions() { + for (String extension : getSourceFileExtensions()) { + excludes.add(Constants.PATTERN + extension); + } + } + + private boolean executePodInstall(String folderToInstall) { + boolean processFailed = false; + CommandLineProcess commandLineProcess = new CommandLineProcess(folderToInstall, new String[]{POD, Constants.INSTALL}); + + try { + commandLineProcess.executeProcess(); + if (commandLineProcess.isErrorInProcess()) { + processFailed = true; + } + } catch (IOException e) { + processFailed = true; + } + return processFailed; + } + + @Override + protected Collection getExcludes() { + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return new ArrayList<>(Arrays.asList(SWIFT_EXT, H_EXT, M_EXT, HPP_EXT, CPP_EXT, CC_EXT, C_EXT)); + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.COCOAPODS; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.COCOAPODS.name(); + } + + @Override + public String[] getBomPattern() { + return new String[]{Constants.GLOB_PATTERN_PREFIX + PODFILE}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(PODFILE); + } + + @Override + protected Collection getLanguageExcludes() { + return new ArrayList<>(); + } + + @Override + protected Collection getRelevantScannedFolders(Collection scannedFolders) { + // CocoaPods resolver should scan all folders and should not remove any folder + return scannedFolders == null ? Collections.emptyList() : scannedFolders; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/DependencyCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/DependencyCollector.java new file mode 100644 index 0000000..d5f89b2 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/DependencyCollector.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; + +import java.util.ArrayList; +import java.util.Collection; +/** + * @author eugen.horovitz + */ +public abstract class DependencyCollector { + + public static final String C_CHAR_WINDOWS = "/c"; + + protected abstract Collection collectDependencies(String folder); + + protected Collection getSingleProjectList(Collection dependencies) { + Collection projects = new ArrayList<>(); + AgentProjectInfo projectInfo = new AgentProjectInfo(); + dependencies.stream().forEach(dependency -> projectInfo.getDependencies().add(dependency)); + projects.add(projectInfo); + return projects; + } + + public static boolean isWindows() { + return System.getProperty(Constants.OS_NAME).toLowerCase().contains(Constants.WIN); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/DependencyResolutionService.java b/src/main/java/org/whitesource/agent/dependency/resolver/DependencyResolutionService.java new file mode 100644 index 0000000..eccfdba --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/DependencyResolutionService.java @@ -0,0 +1,389 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.CocoaPods.CocoaPodsDependencyResolver; +import org.whitesource.agent.dependency.resolver.bower.BowerDependencyResolver; +import org.whitesource.agent.dependency.resolver.dotNet.DotNetDependencyResolver; +import org.whitesource.agent.dependency.resolver.go.GoDependencyResolver; +import org.whitesource.agent.dependency.resolver.gradle.GradleDependencyResolver; +import org.whitesource.agent.dependency.resolver.hex.HexDependencyResolver; +import org.whitesource.agent.dependency.resolver.html.HtmlDependencyResolver; +import org.whitesource.agent.dependency.resolver.maven.MavenDependencyResolver; +import org.whitesource.agent.dependency.resolver.npm.NpmDependencyResolver; +import org.whitesource.agent.dependency.resolver.nuget.NugetDependencyResolver; +import org.whitesource.agent.dependency.resolver.nuget.packagesConfig.NugetConfigFileType; +import org.whitesource.agent.dependency.resolver.php.PhpDependencyResolver; +import org.whitesource.agent.dependency.resolver.python.PythonDependencyResolver; +import org.whitesource.agent.dependency.resolver.ruby.RubyDependencyResolver; +import org.whitesource.agent.dependency.resolver.sbt.SbtDependencyResolver; +import org.whitesource.agent.utils.FilesScanner; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.fs.configuration.ResolverConfiguration; + +import java.nio.file.Path; +import java.util.*; + +/** + * Holds and initiates all {@link AbstractDependencyResolver}s. + * + * @author eugen.horovitz + */ +public class DependencyResolutionService { + + /* --- Members --- */ + + private final Logger logger = LoggerFactory.getLogger(DependencyResolutionService.class); + + private final FilesScanner fileScanner; + private final Collection dependencyResolvers; + private final boolean ignoreSourceFiles; + + private boolean separateProjects; + private boolean mavenAggregateModules; + private boolean sbtAggregateModules; + private boolean gradleAggregateModules; + private boolean hexAggregateModules; + + /* --- Static members --- */ + + public static final List multiModuleDependencyTypes = Arrays.asList(DependencyType.MAVEN, DependencyType.GRADLE); + + /* --- Constructors --- */ + + public DependencyResolutionService(ResolverConfiguration config) { + final boolean npmRunPreStep = config.isNpmRunPreStep(); + final boolean npmIgnoreScripts = config.isNpmIgnoreScripts(); + final boolean npmResolveDependencies = config.isNpmResolveDependencies(); + final boolean npmIncludeDevDependencies = config.isNpmIncludeDevDependencies(); + final long npmTimeoutDependenciesCollector = config.getNpmTimeoutDependenciesCollector(); + final boolean npmIgnoreNpmLsErrors = config.getNpmIgnoreNpmLsErrors(); + final String npmAccessToken = config.getNpmAccessToken(); + final boolean npmYarnProject = config.getNpmYarnProject(); + final boolean npmIgnoreSourceFiles = config.isNpmIgnoreSourceFiles(); + + final boolean bowerResolveDependencies = config.isBowerResolveDependencies(); + final boolean bowerRunPreStep = config.isBowerRunPreStep(); + final boolean bowerIgnoreSourceFiles = config.isBowerIgnoreSourceFiles(); + + final boolean nugetResolveDependencies = config.isNugetResolveDependencies(); + final boolean nugetRestoreDependencies = config.isNugetRestoreDependencies(); + final boolean nugetRunPreStep = config.isNugetRunPreStep(); + final boolean nugetIgnoreSourceFiles = config.isNugetIgnoreSourceFiles(); + final boolean nugetResolveCsProjFiles = config.isNugetResolveCsProjFiles(); + final boolean nugetResolvePackagesConfigFiles = config.isNugetResolvePackagesConfigFiles(); + + final boolean mavenResolveDependencies = config.isMavenResolveDependencies(); + final String[] mavenIgnoredScopes = config.getMavenIgnoredScopes(); + final boolean mavenAggregateModules = config.isMavenAggregateModules(); + final boolean mavenIgnorePomModules = config.isMavenIgnorePomModules(); + final boolean mavenIgnoreSourceFiles = config.isMavenIgnoreSourceFiles(); + final boolean mavenRunPreStep = config.isMavenRunPreStep(); + final boolean mavenIgnoreDependencyTreeErrors = config.isMavenIgnoreDependencyTreeErrors(); + + boolean pythonResolveDependencies = config.isPythonResolveDependencies(); + final String[] pythonRequirementsFileIncludes = config.getPythonRequirementsFileIncludes(); + final boolean pythonIgnoreSourceFiles = config.isPythonIgnoreSourceFiles(); + final boolean ignorePipEnvInstallErrors = config.isIgnorePipEnvInstallErrors(); + final boolean runPipenvPreStep = config.IsRunPipenvPreStep(); + final boolean pipenvInstallDevDependencies = config.isPipenvInstallDevDependencies(); + + + boolean gradleResolveDependencies = config.isGradleResolveDependencies(); + boolean gradleAggregateModules = config.isGradleAggregateModules(); + final boolean gradleIgnoreSourceFiles = config.isGradleIgnoreSourceFiles(); + boolean gradleRunPreStep = config.isGradleRunPreStep(); + final String[] gradleIgnoredScopes = config.getGradleIgnoredScopes(); + final String gradleLocalRepositoryPath = config.getGradleLocalRepositoryPath(); + + final boolean paketResolveDependencies = config.isPaketResolveDependencies(); + final String[] paketIgnoredScopes = config.getPaketIgnoredScopes(); + final boolean paketRunPreStep = config.isPaketRunPreStep(); + final String paketPath = config.getPaketPath(); + final boolean paketIgnoreSourceFiles = config.isPaketIgnoreSourceFiles(); + + + final boolean goResolveDependencies = config.isGoResolveDependencies(); + final boolean goIgnoreSourceFiles = config.isGoGlideIgnoreSourceFiles(); + + final boolean rubyResolveDependencies = config.isRubyResolveDependencies(); + final boolean rubyRunBundleInstall = config.isRubyRunBundleInstall(); + final boolean rubyOverwriteGemFile = config.isRubyOverwriteGemFile(); + final boolean rubyInstallMissingGems = config.isRubyInstallMissingGems(); + final boolean rubyIgnoreSourceFiles = config.isRubyIgnoreSourceFiles(); + + final boolean phpResolveDependencies = config.isPhpResolveDependencies(); + final boolean phpRunPreStep = config.isPhpRunPreStep(); + final boolean phpIncludeDevDependencies = config.isPhpIncludeDevDependencies(); + + final boolean sbtResolveDependencies = config.isSbtResolveDependencies(); + final boolean sbtAggregateModules = config.isSbtAggregateModules(); + final boolean sbtRunPreStep = config.isSbtRunPreStep(); + final String sbtTargetFolder = config.getSbtTargetFolder(); + final boolean sbtIgnoreSourceFiles = config.isSbtIgnoreSourceFiles(); + + final boolean htmlResolveDependencies = config.isHtmlResolveDependencies(); + + final boolean cocoapodsResolveDependencies = config.isCocoapodsResolveDependencies(); + final boolean cocoapodsRunPreStep = config.isCocoapodsRunPreStep(); + final boolean cocoapodsIgnoreSourceFiles = config.isCocoapodsIgnoreSourceFiles(); + + final boolean hexResolveDependencies = config.isHexResolveDependencies(); + final boolean hexRunPreStep = config.isHexRunPreStep(); + final boolean hexAggregateModules = config.isHexAggregateModules(); + final boolean hexIgnoreSourceFiles = config.isHexIgnoreSourceFiles(); + + ignoreSourceFiles = config.isIgnoreSourceFiles(); + + fileScanner = new FilesScanner(); + dependencyResolvers = new ArrayList<>(); + if (npmResolveDependencies) { + dependencyResolvers.add(new NpmDependencyResolver(npmIncludeDevDependencies, npmIgnoreSourceFiles, npmTimeoutDependenciesCollector, npmRunPreStep, npmIgnoreNpmLsErrors, + npmAccessToken, npmYarnProject, npmIgnoreScripts)); + } + if (bowerResolveDependencies) { + dependencyResolvers.add(new BowerDependencyResolver(npmTimeoutDependenciesCollector, bowerRunPreStep, bowerIgnoreSourceFiles)); + } + if (nugetResolveDependencies) { + String whitesourceConfiguration = config.getWhitesourceConfiguration(); + if (nugetResolvePackagesConfigFiles) { + dependencyResolvers.add(new NugetDependencyResolver(whitesourceConfiguration, NugetConfigFileType.CONFIG_FILE_TYPE, nugetRunPreStep, nugetIgnoreSourceFiles)); + } + if (nugetResolveCsProjFiles) { + dependencyResolvers.add(new DotNetDependencyResolver(whitesourceConfiguration, NugetConfigFileType.CSPROJ_TYPE, nugetRestoreDependencies, nugetIgnoreSourceFiles)); + } + } + if (mavenResolveDependencies) { + dependencyResolvers.add(new MavenDependencyResolver(mavenAggregateModules, mavenIgnoredScopes, mavenIgnoreSourceFiles, mavenIgnorePomModules, mavenRunPreStep, + mavenIgnoreDependencyTreeErrors)); + this.mavenAggregateModules = mavenAggregateModules; + } + if (pythonResolveDependencies) { + dependencyResolvers.add(new PythonDependencyResolver(config.getPythonPath(), config.getPipPath(), + config.isPythonIgnorePipInstallErrors(), config.isPythonInstallVirtualenv(), config.isPythonResolveHierarchyTree(), pythonRequirementsFileIncludes, + pythonIgnoreSourceFiles, ignorePipEnvInstallErrors, runPipenvPreStep, pipenvInstallDevDependencies)); + } + + if (gradleResolveDependencies) { + dependencyResolvers.add(new GradleDependencyResolver(config.isGradleRunAssembleCommand(), gradleIgnoreSourceFiles, gradleAggregateModules, + config.getGradlePreferredEnvironment(), gradleIgnoredScopes, gradleLocalRepositoryPath, gradleRunPreStep)); + this.gradleAggregateModules = gradleAggregateModules; + } + + + + if (goResolveDependencies) { + dependencyResolvers.add(new GoDependencyResolver(config.getGoDependencyManager(), config.isGoCollectDependenciesAtRuntime(), goIgnoreSourceFiles, + config.isGoGlideIgnoreTestPackages(), config.isGoGradleEnableTaskAlias(), config.getGradlePreferredEnvironment(), config.isAddSha1())); + } + + if (rubyResolveDependencies) { + dependencyResolvers.add(new RubyDependencyResolver(rubyRunBundleInstall, rubyOverwriteGemFile, rubyInstallMissingGems, rubyIgnoreSourceFiles)); + } + + if (phpResolveDependencies) { + dependencyResolvers.add(new PhpDependencyResolver(phpRunPreStep, phpIncludeDevDependencies, config.isAddSha1())); + } + + if (htmlResolveDependencies) { + dependencyResolvers.add(new HtmlDependencyResolver()); + } + + if (sbtResolveDependencies) { + dependencyResolvers.add(new SbtDependencyResolver(sbtAggregateModules, sbtIgnoreSourceFiles, sbtRunPreStep, sbtTargetFolder)); + this.sbtAggregateModules = sbtAggregateModules; + } + + if (cocoapodsResolveDependencies) { + dependencyResolvers.add(new CocoaPodsDependencyResolver(cocoapodsRunPreStep, cocoapodsIgnoreSourceFiles)); + } + + if (hexResolveDependencies) { + dependencyResolvers.add(new HexDependencyResolver(hexIgnoreSourceFiles, hexRunPreStep, hexAggregateModules)); + this.hexAggregateModules = hexAggregateModules; + } + + this.separateProjects = false; + } + + /* --- Public methods --- */ + + public boolean isMavenAggregateModules() { + return mavenAggregateModules; + } + + public boolean isSbtAggregateModules() { + return sbtAggregateModules; + } + + public boolean isGradleAggregateModules() { + return gradleAggregateModules; + } + + public boolean isHexAggregateModules() { + return hexAggregateModules; + } + + public boolean isSeparateProjects() { + return separateProjects; + } + + public boolean isIgnoreSourceFiles() { + return ignoreSourceFiles; + } + + public boolean shouldResolveDependencies(Set allFoundFiles) { + for (AbstractDependencyResolver dependencyResolver : dependencyResolvers) { + for (String bomFile : dependencyResolver.getManifestFiles()) { + boolean shouldResolve = allFoundFiles.stream().filter(file -> file.endsWith(bomFile)).findAny().isPresent(); + if (shouldResolve) { + return true; + } + } + } + return false; + } + + public List resolveDependencies(Collection pathsToScan, String[] excludes) { + Map topFolderResolverMap = new HashMap<>(); + Collection multiModuleResults = new LinkedList<>(); + Collection htmlResults = new LinkedList<>(); + + dependencyResolvers.forEach(dependencyResolver -> { + // add resolver excludes + Collection combinedExcludes = new LinkedList<>(Arrays.asList(excludes)); + Collection resolverExcludes = dependencyResolver.getExcludes(); + for (String exclude : resolverExcludes) { + combinedExcludes.add(exclude); + } + logger.debug("Attempting to find the top folders of {} with pattern {}", pathsToScan, dependencyResolver.getBomPattern()); + Collection topFolders = fileScanner.findTopFolders(pathsToScan, dependencyResolver.getBomPattern(), combinedExcludes); + topFolders.forEach(topFolder -> topFolderResolverMap.put(topFolder, dependencyResolver)); + }); + logger.debug("Attempting to reduce dependencies"); + // reduce the dependencies and duplicates files + reduceDependencies(topFolderResolverMap); + + logger.debug("Finishing reduce dependencies"); + List resolutionResults = new ArrayList<>(); + + + topFolderResolverMap.forEach((resolvedFolder, dependencyResolver) -> { + if (!resolvedFolder.getTopFoldersFound().isEmpty()) { + logger.info("Trying to resolve " + dependencyResolver.getDependencyTypeName() + " dependencies"); + } + resolvedFolder.getTopFoldersFound().forEach((topFolder, bomFiles) -> { + // don't print folder in case of html resolution + if (dependencyResolver.printResolvedFolder()) { + logger.info("topFolder = " + topFolder); + } + logger.debug("topFolder = " + topFolder); + ResolutionResult result = null; + try { + result = dependencyResolver.resolveDependencies(resolvedFolder.getOriginalScanFolder(), topFolder, bomFiles); + } catch (Exception e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } + if (result != null) { + resolutionResults.add(result); + + // create lists in order to match htmlResolver dependencies to their original project (Maven/Gradle/Sbt) + if (multiModuleDependencyTypes.contains(dependencyResolver.getDependencyType())) { + multiModuleResults.add(result); + + } else if (Constants.HTML.toUpperCase().equals(dependencyResolver.getDependencyTypeName())) { + htmlResults.add(result); + } + } + }); + }); + // match htmlResolver dependencies to their original project (Maven/Gradle/Sbt) + findAndSetHtmlProject(multiModuleResults, htmlResults, resolutionResults); + return resolutionResults; + } + + private void findAndSetHtmlProject(Collection multiModuleResults, Collection htmlResults, + Collection resolutionResults) { + if (!(multiModuleResults.isEmpty() && htmlResults.isEmpty())) { + // parameters initialization + Map agentProjectInfoToResolutionResult = new HashMap<>(); + Map multiModuleProjects = generateMultiProjectMap(multiModuleResults, agentProjectInfoToResolutionResult); + Map htmlProjects = generateMultiProjectMap(htmlResults, agentProjectInfoToResolutionResult); + // Match for each html project its original project & remove its "fake" project from the list. + for (Map.Entry multiModuleProject : multiModuleProjects.entrySet()) { + for (Map.Entry htmlProject : htmlProjects.entrySet()) { + if (htmlProject.getValue().toAbsolutePath().toString().contains(multiModuleProject.getValue().toAbsolutePath().toString())) { + ResolutionResult htmlResult = agentProjectInfoToResolutionResult.get(htmlProject.getKey()); + resolutionResults.remove(htmlResult); + multiModuleProject.getKey().getDependencies().addAll(htmlProject.getKey().getDependencies()); + ResolutionResult multiModuleResult = agentProjectInfoToResolutionResult.get(htmlProject.getKey()); + multiModuleResult.getResolvedProjects().put(multiModuleProject.getKey(), multiModuleProject.getValue()); + } + } + } + } + } + + private Map generateMultiProjectMap(Collection projectResults, Map agentProjectInfoToResolutionResult) { + Map resolvedProjects = new HashMap<>(); + // initialize projectResults & resolvedProjects + for (ResolutionResult result : projectResults) { + Map projects = result.getResolvedProjects(); + for (Map.Entry project : projects.entrySet()) { + agentProjectInfoToResolutionResult.put(project.getKey(), result); + } + resolvedProjects.putAll(projects); + } + return resolvedProjects; + } + + public Collection getDependencyResolvers() { + return dependencyResolvers; + } + + /* --- Private methods --- */ + private void reduceDependencies(Map topFolderResolverMap) { + //reduce the dependencies and duplicates files + Set topFolders = new HashSet<>(); + topFolderResolverMap.entrySet().forEach((resolverEntry) -> topFolders.addAll(resolverEntry.getKey().getTopFoldersFound().keySet())); + + // Take all resolvers and their folders + for (Map.Entry entry : topFolderResolverMap.entrySet()) { + AbstractDependencyResolver resolver = entry.getValue(); + ResolvedFolder resolvedFolder = entry.getKey(); + if (resolver != null && resolvedFolder != null) { + // All folders of the current resolver (top folders and sub folders) + Set foldersFound = resolvedFolder.getTopFoldersFound().keySet(); + // Get the relevant folders for the current resolver (can be all folders or top folders only ...) + Collection foldersForResolver = resolver.getRelevantScannedFolders(foldersFound); + // Remove all the irrelevant folders + resolvedFolder.getTopFoldersFound().keySet().removeIf(folder -> !foldersForResolver.contains(folder)); + } + } + + } + + private boolean isChildFolder(String childFolder, String topFolderParent) { + boolean result = childFolder.contains(topFolderParent) && !childFolder.equals(topFolderParent); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/IBomParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/IBomParser.java new file mode 100644 index 0000000..43044d5 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/IBomParser.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +/** + * @author eugen.horovitz + */ +public interface IBomParser { + BomFile parseBomFile(String bomPath); +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/ResolutionResult.java b/src/main/java/org/whitesource/agent/dependency/resolver/ResolutionResult.java new file mode 100644 index 0000000..ebfc148 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/ResolutionResult.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by eugen on 6/21/2017. + */ +public class ResolutionResult { + + /* --- Members --- */ + + private Map resolvedProjects; + private Collection excludes; + private final DependencyType dependencyType; + private final String topLevelFolder; + + /* --- Constructors --- */ + + public ResolutionResult(Map resolvedProjects, Collection excludes, DependencyType dependencyType, String topLevelFolder) { + this.resolvedProjects = resolvedProjects; + this.excludes = excludes; + this.dependencyType = dependencyType; + this.topLevelFolder = topLevelFolder; + } + + public ResolutionResult(Collection dependencies, Iterable excludes, DependencyType dependencyType, String topLevelFolder) { + AgentProjectInfo projectInfo = new AgentProjectInfo(); + dependencies.forEach(dependencyInfo -> projectInfo.getDependencies().add(dependencyInfo)); + + this.resolvedProjects = new HashMap<>(); + this.resolvedProjects.put(projectInfo, Paths.get(topLevelFolder)); + this.excludes = new ArrayList<>(); + this.dependencyType = dependencyType; + this.topLevelFolder = topLevelFolder; + excludes.forEach(exclude -> this.excludes.add(exclude)); + } + + /* --- Getters --- */ + + public Collection getExcludes() { + return excludes; + } + + public Map getResolvedProjects() { + return resolvedProjects; + } + + public DependencyType getDependencyType() { + return dependencyType; + } + + public String getTopLevelFolder() { + return topLevelFolder; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/ResolvedFolder.java b/src/main/java/org/whitesource/agent/dependency/resolver/ResolvedFolder.java new file mode 100644 index 0000000..234b428 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/ResolvedFolder.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver; + +import java.util.Map; +import java.util.Set; + +/** + * @author eugen.horovitz + */ +public class ResolvedFolder { + + /* --- Members --- */ + + private final String originalScanFolder; + private final Map> topFoldersFound; + + /* --- Constructors --- */ + + public ResolvedFolder(String originalScanFolder, Map> topFoldersFound) { + this.originalScanFolder = originalScanFolder; + this.topFoldersFound = topFoldersFound; + } + + /* --- Getters --- */ + + public String getOriginalScanFolder() { + return originalScanFolder; + } + + public Map> getTopFoldersFound() { + return topFoldersFound; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/ViaMultiModuleAnalyzer.java b/src/main/java/org/whitesource/agent/dependency/resolver/ViaMultiModuleAnalyzer.java new file mode 100644 index 0000000..ad9a850 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/ViaMultiModuleAnalyzer.java @@ -0,0 +1,121 @@ +package org.whitesource.agent.dependency.resolver; + +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.utils.FilesScanner; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author raz.nitzan + */ +public class ViaMultiModuleAnalyzer { + + /* --- Static Members --- */ + + private static final String APP_PATH = "AppPath"; + private static final String DEPENDENCY_MANAGER_PATH = "DependencyManagerFilePath"; + private static final String PROJECT_FOLDER_PATH = "ProjectFolderPath"; + private static final String DEFAULT_NAME = "defaultName"; + private static final String ALT_NAME = "altName"; + + /* --- Members --- */ + + private final Logger logger = LoggerFactory.getLogger(ViaMultiModuleAnalyzer.class); + private final Collection buildExtensions = new HashSet<>(Arrays.asList(".jar", ".war", ".zip")); + private AbstractDependencyResolver dependencyResolver; + private String suffixOfBuild; + private String scanDirectory; + private String contentFileAppPaths; + private Collection bomFiles = new HashSet<>(); + + /* --- Constructor --- */ + + public ViaMultiModuleAnalyzer(String scanDirectory, AbstractDependencyResolver dependencyResolver, String suffixOfBuild, String contentFileAppPaths) { + this.dependencyResolver = dependencyResolver; + this.suffixOfBuild = suffixOfBuild; + this.scanDirectory = scanDirectory; + this.contentFileAppPaths = contentFileAppPaths; + findBomFiles(); + } + + private void findBomFiles() { + Collection scanDirectoryCollection = new LinkedList<>(); + scanDirectoryCollection.add(scanDirectory); + Collection topFolders = new FilesScanner().findTopFolders(scanDirectoryCollection, dependencyResolver.getBomPattern(), dependencyResolver.getExcludes()); + topFolders.forEach(topFolder -> topFolder.getTopFoldersFound().forEach((folder, bomFilesFound) -> this.bomFiles.addAll(bomFilesFound))); + } + + public void writeFile() { + try { + File outputFile = new File(this.contentFileAppPaths); + FileOutputStream fileOutputStream = new FileOutputStream(outputFile); + BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream)); + bufferedWriter.write(replaceAllSlashes(DEPENDENCY_MANAGER_PATH + Constants.EQUALS + this.scanDirectory)); + bufferedWriter.write(System.lineSeparator()); + int counter = 1; + boolean printMessageAppPath = true; + HashMap folderNameCounter = new HashMap<>(); + for (String bomFile : bomFiles) { + File parentFileOfBom = new File(bomFile).getParentFile(); + String parentFileName = parentFileOfBom.getName(); + File buildFolder = new File(parentFileOfBom.getPath() + File.separator + this.suffixOfBuild); + if (buildFolder.exists() && buildFolder.isDirectory() && buildFolder.listFiles() != null) { + Collection filesWithBuildExtensions = Arrays.stream(buildFolder.listFiles()).filter(file -> { + for (String extension : buildExtensions) { + if (file.getName().endsWith(extension)) { + return true; + } + } + return false; + }).collect(Collectors.toList()); + try { + if (filesWithBuildExtensions.size() >= 1) { + bufferedWriter.write(replaceAllSlashes(PROJECT_FOLDER_PATH + counter + Constants.EQUALS + parentFileOfBom.getAbsolutePath())); + bufferedWriter.write(System.lineSeparator()); + String appPathProperty = APP_PATH + counter + Constants.EQUALS; + if (filesWithBuildExtensions.size() == 1) { + File appPath = filesWithBuildExtensions.stream().findFirst().get(); + appPathProperty += appPath.getAbsolutePath(); + } else if (printMessageAppPath) { + logger.warn("Analysis found multiple candidates for one or more appPath settings that are listed in the multi-module analysis setup file. Please review the setup file and set the appropriate appPath parameters."); + printMessageAppPath = false; + } + bufferedWriter.write(replaceAllSlashes(appPathProperty)); + bufferedWriter.write(System.lineSeparator()); + bufferedWriter.write(replaceAllSlashes(DEFAULT_NAME + counter + Constants.EQUALS + parentFileName)); + bufferedWriter.write(System.lineSeparator()); + if (folderNameCounter.get(parentFileName) == null){ + folderNameCounter.put(parentFileName,0); + bufferedWriter.write(replaceAllSlashes(ALT_NAME + counter + Constants.EQUALS + parentFileName)); + } else { + int i = folderNameCounter.get(parentFileName) + 1; + folderNameCounter.put(parentFileName,i); + bufferedWriter.write(replaceAllSlashes(ALT_NAME + counter + Constants.EQUALS + parentFileName + Constants.UNDERSCORE + i)); + } + bufferedWriter.write(System.lineSeparator()); + counter++; + } + } catch (IOException e) { + logger.warn("Failed to write to file: {}", this.contentFileAppPaths); + } + } + } + bufferedWriter.flush(); + bufferedWriter.close(); + } catch (IOException e) { + logger.warn("Failed to write to file: {}", this.contentFileAppPaths); + } + } + + public Collection getBomFiles() { + return this.bomFiles; + } + + private String replaceAllSlashes(String line) { + return line.replaceAll("\\\\", "/"); + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerBomParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerBomParser.java new file mode 100644 index 0000000..c2a6bf8 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerBomParser.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.bower; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.npm.NpmBomParser; + +import java.text.MessageFormat; + +/** + * This class represents an Bower .bower.json file . + * When missing bower.json is parsed + * + * @author eugen.horovitz + */ +public class BowerBomParser extends NpmBomParser { + + /* --- Static members --- */ + + public static final String RESOLUTION = "_resolution"; + public static final String BOWER_PACKAGE_FILENAME_FORMAT = "{0}-{1}"; + private final Logger logger = LoggerFactory.getLogger(NpmBomParser.class); + + /* --- Overridden methods --- */ + + @Override + protected String getVersion(JSONObject json, String fileName) { + String version = Constants.EMPTY_STRING; + if (json.has(RESOLUTION)) { + JSONObject jObj = json.getJSONObject(RESOLUTION); + if (jObj.has(Constants.TAG)) { + return jObj.getString(Constants.TAG); + } + logger.debug("version not found in file {}", fileName); + return Constants.EMPTY_STRING; + } + return version; + } + + @Override + protected String getFilename(String name, String version) { + return MessageFormat.format(BOWER_PACKAGE_FILENAME_FORMAT, name, version); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerDependencyResolver.java new file mode 100644 index 0000000..38e17c9 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerDependencyResolver.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.bower; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.BomFile; +import org.whitesource.agent.dependency.resolver.npm.NpmDependencyResolver; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + + +/** + * Dependency Resolver for BOWER projects. + * + * @author eugen.horovitz + */ +public class BowerDependencyResolver extends NpmDependencyResolver { + + /* --- Static Members --- */ + + private static final String BOWER_JSON = "bower.json"; + public static final String WS_BOWER_FILE2 = Constants.PATTERN + "ws_bower.json"; + public static final String WS_BOWER_FILE1 = Constants.PATTERN + "ws-log-response-bower.json"; + + /* --- Members --- */ + + private final BowerBomParser bomParser; + private final BowerLsJsonDependencyCollector bomCollector; + + /* --- Constructor --- */ + + public BowerDependencyResolver(long npmTimeoutDependenciesCollector, boolean runPreStep, boolean ignoreSourceFiles) { + super(runPreStep, null, ignoreSourceFiles); + bomParser = new BowerBomParser(); + bomCollector = new BowerLsJsonDependencyCollector(npmTimeoutDependenciesCollector); + } + + /* --- Overridden methods --- */ + + @Override + protected BowerLsJsonDependencyCollector getDependencyCollector() { + return bomCollector; + } + + @Override + public String[] getBomPattern() { + return new String[]{Constants.PATTERN + BOWER_JSON}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(BOWER_JSON); + } + + @Override + public String getPreferredFileName() { + return Constants.DOT + BOWER_JSON; + } + + @Override + protected BowerBomParser getBomParser() { + return bomParser; + } + + @Override + protected void enrichDependency(DependencyInfo dependency, BomFile packageJson, String npmAccessToken) { + dependency.setGroupId(packageJson.getName()); + dependency.setArtifactId(packageJson.getName()); + dependency.setVersion(packageJson.getVersion()); + dependency.setSystemPath(packageJson.getLocalFileName()); + dependency.setFilename(packageJson.getFileName()); + dependency.setDependencyType(getDependencyType()); + } + + @Override + protected Collection getLanguageExcludes() { + // exclude files generated by the WhiteSource Bower plugin + Set excludes = new HashSet<>(); + excludes.add(WS_BOWER_FILE2); + excludes.add(WS_BOWER_FILE1); + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return Arrays.asList(BOWER_JSON); + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.BOWER; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.BOWER.name(); + } + + @Override + protected boolean isMatchChildDependency(DependencyInfo childDependency, String name, String version) { + return childDependency.getGroupId().equals(name) && childDependency.getVersion().equals(version); + } + + +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerLsJsonDependencyCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerLsJsonDependencyCollector.java new file mode 100644 index 0000000..2ca8a18 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/bower/BowerLsJsonDependencyCollector.java @@ -0,0 +1,138 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.bower; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.npm.NpmLsJsonDependencyCollector; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Collect dependencies using 'bower ls' command. + * + * @author eugen.horovitz + */ +public class BowerLsJsonDependencyCollector extends NpmLsJsonDependencyCollector { + + /* --- Statics Members --- */ + + private final Logger logger = LoggerFactory.getLogger(BowerLsJsonDependencyCollector.class); + private static final String BOWER_COMMAND = NpmLsJsonDependencyCollector.isWindows() ? "bower.cmd" : "bower"; + private static final String PKG_META = "pkgMeta"; + private static final String TYPE = "type"; + + /* --- Constructors --- */ + + public BowerLsJsonDependencyCollector(long npmTimeoutDependenciesCollector) { + super(false, npmTimeoutDependenciesCollector, false, false); + } + + /* --- Overridden methods --- */ + + @Override + protected String[] getInstallParams() { + return new String[]{BOWER_COMMAND, Constants.INSTALL}; + } + + @Override + protected String[] getLsCommandParamsJson() { + return new String[]{BOWER_COMMAND, NpmLsJsonDependencyCollector.LS_COMMAND, NpmLsJsonDependencyCollector.LS_PARAMETER_JSON}; + } + + @Override + protected DependencyInfo getDependency(String dependencyAlias, JSONObject jsonObject) { + String version = Constants.EMPTY_STRING; + String name = Constants.EMPTY_STRING; + boolean unmetDependency = false; + + if (jsonObject.has(Constants.MISSING) && jsonObject.getBoolean(Constants.MISSING)) { + unmetDependencyLog(dependencyAlias); + return null; + } + if (jsonObject.has(PKG_META)) { + JSONObject metaData = jsonObject.getJSONObject(PKG_META); + if (metaData.has(Constants.RESOLUTION)) { + JSONObject resolution = metaData.getJSONObject(Constants.RESOLUTION); + String resolutionType = resolution.getString(TYPE); + if (metaData.has(Constants.NAME)) { + name = metaData.getString(Constants.NAME); + } else { + unmetDependency = true; + } + if (resolutionType.equals(Constants.TAG) || resolutionType.equals(Constants.VERSION)) { + version = metaData.getString(Constants.VERSION); + } else { + logger.warn("We were not able to allocate the bower version for '{}' in you bower.json file." + + "At the moment we only support tag, so please modify your bower.json " + + "accordingly and run the plugin again.", name); + return null; + } + } else { + unmetDependency = true; + } + } else { + unmetDependency = true; + } + + if (unmetDependency) { + unmetDependencyLog(dependencyAlias); + return null; + } + + DependencyInfo dependency = new DependencyInfo(); + dependency.setGroupId(name); + dependency.setArtifactId(name); + dependency.setVersion(version); + dependency.setDependencyType(DependencyType.BOWER); + return dependency; + } + + @Override + protected void getDependencies(JSONObject jsonObject, String rootDirectory, Collection dependencies) { + if (jsonObject.has(Constants.DEPENDENCIES)) { + JSONObject dependenciesJsonObject = jsonObject.getJSONObject(Constants.DEPENDENCIES); + if (dependenciesJsonObject != null) { + for (String dependencyAlias : dependenciesJsonObject.keySet()) { + JSONObject dependencyJsonObject = dependenciesJsonObject.getJSONObject(dependencyAlias); + if (dependencyJsonObject.keySet().isEmpty()) { + logger.debug("Dependency {} has no JSON content", dependencyAlias); + } else { + DependencyInfo dependency = getDependency(dependencyAlias, dependencyJsonObject); + if (dependency != null) { + dependencies.add(dependency); + + logger.debug("Collect child dependencies of {}", dependencyAlias); + // collect child dependencies + Collection childDependencies = new ArrayList<>(); + getDependencies(dependencyJsonObject, rootDirectory, childDependencies); + dependency.getChildren().addAll(childDependencies); + } + } + } + } + } + } + + private void unmetDependencyLog(String dependencyAlias) { + logger.warn("Unmet dependency --> {}", dependencyAlias); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/AbstractParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/AbstractParser.java new file mode 100644 index 0000000..a6b89b3 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/AbstractParser.java @@ -0,0 +1,60 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import org.slf4j.Logger; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Collection; + +/** + * @author chen.luigi + */ +public abstract class AbstractParser { + + /* --- Constructors --- */ + protected static final Logger logger = LoggerFactory.getLogger(AbstractParser.class); + + public AbstractParser() { + + } + + /* --- Public methods --- */ + + static void closeStream(BufferedReader br, FileReader fr) { + try { + if (br != null) + br.close(); + if (fr != null) + fr.close(); + } catch (IOException ex) { + logger.error(ex.getMessage()); + logger.debug("{}", ex.getStackTrace()); + } + } + + /* --- Abstract methods --- */ + + public abstract Collection parse(File file); + + public abstract File findFile(String[] files, String filename); + + public static void findFolder(File dir, String folderName, Collection folder) { + if (dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + findFolder(file, folderName, folder); + if (file.getName().equals(folderName)) { + folder.add(file.getPath()); + } + } + } + } + } + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/AlpineParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/AlpineParser.java new file mode 100644 index 0000000..6cdbc39 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/AlpineParser.java @@ -0,0 +1,100 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import org.apache.commons.lang.StringUtils; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; + +import java.io.*; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.LinkedList; + +/** + * @author chen.luigi + */ +public class AlpineParser extends AbstractParser { + + + /* --- Static members --- */ + + private static final String PACKAGE = "P"; + private static final String VERSION = "V"; + private static final String ARCHITECTURE = "A"; + private static final String ALPINE_PACKAGE_PATTERN = "{0}.apk"; + + /* --- Overridden methods --- */ + + @Override + public Collection parse(File file) { + BufferedReader br = null; + FileReader fr = null; + Collection dependencyInfos = new LinkedList<>(); + try { + fr = new FileReader(file.getAbsoluteFile()); + br = new BufferedReader(fr); + String line = null; + Package packageInfo = new Package(); + // Create Alpine package - package-version-architecture.apk + while ((line = br.readLine()) != null) { + if (!line.isEmpty()) { + if (packageInfo.getPackageName() == null || packageInfo.getVersion() == null || packageInfo.getArchitecture() == null) { + String[] lineSplit = line.split(Constants.COLON); + String dependencyParameter = lineSplit[1].trim(); + switch (lineSplit[0]) { + case PACKAGE: + packageInfo.setPackageName(dependencyParameter); + break; + case VERSION: + packageInfo.setVersion(dependencyParameter); + break; + case ARCHITECTURE: + packageInfo.setArchitecture(dependencyParameter); + break; + default: + break; + } + } + } else { + DependencyInfo dependencyInfo = createDependencyInfo(packageInfo); + packageInfo = new Package(); + dependencyInfos.add(dependencyInfo); + } + } + } catch (Exception e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } finally { + closeStream(br, fr); + } + return dependencyInfos; + } + + @Override + public File findFile(String[] files, String filename) { + for (String filepath : files) { + if (filepath.endsWith(filename)) { + return new File(filepath); + } + } + return null; + } + + /* --- Private methods --- */ + + private DependencyInfo createDependencyInfo(Package packageInfo) { + DependencyInfo dependencyInfo = null; + if (StringUtils.isNotBlank(packageInfo.getPackageName()) && StringUtils.isNotBlank(packageInfo.getVersion()) && + StringUtils.isNotBlank(packageInfo.getArchitecture())) { + dependencyInfo = new DependencyInfo( + null, MessageFormat.format(ALPINE_PACKAGE_PATTERN, packageInfo.getPackageName() + Constants.DASH + + packageInfo.getVersion()), packageInfo.getVersion() + Constants.DASH + + packageInfo.getArchitecture()); + } + if (dependencyInfo != null) { + return dependencyInfo; + } else { + return null; + + } + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/ArchLinuxParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/ArchLinuxParser.java new file mode 100644 index 0000000..14a790c --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/ArchLinuxParser.java @@ -0,0 +1,137 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.api.model.DependencyInfo; + +import java.io.*; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.LinkedList; + + +/** + * @author chen.luigi + */ +public class ArchLinuxParser extends AbstractParser { + + /* --- Static members --- */ + private final Logger logger = LoggerFactory.getLogger(ArchLinuxParser.class); + + private static final String PACKAGE = "%NAME%"; + private static final String VERSION = "%VERSION%"; + private static final String ARCHITECTURE = "%ARCH%"; + private static final String DESC = "desc"; + private static final String ARCH_LINUX_PACKAGE_PATTERN = "{0}-{1}-{2}.pkg.tar.xz"; + + /* --- Overridden methods --- */ + + @Override + public Collection parse(File dir) { + BufferedReader br = null; + FileReader fr = null; + Collection dependencyInfos = new LinkedList<>(); + if (dir.isDirectory()) { + Collection files = new LinkedList<>(); + getDescFiles(dir, files); + if (!files.isEmpty()) { + for (File file : files) { + try { + DependencyInfo dependencyInfo = null; + Package packageInfo = new Package(); + fr = new FileReader(file); + br = new BufferedReader(fr); + String line = null; + // Create Arch Linux package - package-version-architecture.pkg.tar.xz + while ((line = br.readLine()) != null) { + switch (line) { + case PACKAGE: + packageInfo.setPackageName(br.readLine()); + break; + case VERSION: + packageInfo.setVersion(br.readLine()); + break; + case ARCHITECTURE: + packageInfo.setArchitecture(br.readLine()); + break; + default: + break; + } + } + dependencyInfos.add(createDependencyInfo(packageInfo)); + } catch (FileNotFoundException e) { + logger.error("Error getting package data", e.getMessage()); + } catch (IOException e) { + logger.error("Error getting package data", e.getMessage()); + } finally { + closeStream(br, fr); + } + } + } + } + return dependencyInfos; + } + + /** + * @param files - list of files to look for + * @param pathToPackageManagerFolder the relevant path for the folder with all the installed packages + * @return Folder file with all the information about the installed packages + */ + @Override + public File findFile(String[] files, String pathToPackageManagerFolder) { + int max = 0; + File archLinuxPackageManagerFile = null; + for (String filepath : files) { + if (filepath.contains(pathToPackageManagerFolder) && filepath.endsWith(DESC)) { + int descStartIndex = filepath.lastIndexOf(pathToPackageManagerFolder); + if (descStartIndex > 0) { + String descPath = filepath.substring(0, descStartIndex + pathToPackageManagerFolder.length()); + File file = new File(descPath); + if (file.listFiles() != null) { + if (max < file.listFiles().length) { + max = file.listFiles().length; + archLinuxPackageManagerFile = file; + } + } + } + } + } + if (archLinuxPackageManagerFile != null) { + return archLinuxPackageManagerFile; + } + return null; + } + + /* --- Private methods --- */ + + // Get the desc files from specific folder (every dec contains dependency info data) + private void getDescFiles(File dir, Collection files) { + if (dir.isDirectory()) { + for (File file : dir.listFiles()) { + if (file.isDirectory()) { + getDescFiles(file, files); + } else if (file.getName().equals(DESC)) { + files.add(file); + } + } + } + } + + private DependencyInfo createDependencyInfo(Package packageInfo) { + DependencyInfo dependencyInfo = null; + if (StringUtils.isNotBlank(packageInfo.getPackageName()) && StringUtils.isNotBlank(packageInfo.getVersion()) + && StringUtils.isNotBlank(packageInfo.getArchitecture())) { + dependencyInfo = new DependencyInfo( + null, MessageFormat.format(ARCH_LINUX_PACKAGE_PATTERN, packageInfo.getPackageName(), + packageInfo.getVersion(), packageInfo.getArchitecture()), packageInfo.getVersion()); + } + if (dependencyInfo != null) { + return dependencyInfo; + } else { + return null; + + } + } + +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/DebianParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/DebianParser.java new file mode 100644 index 0000000..261550f --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/DebianParser.java @@ -0,0 +1,121 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; + +import java.io.*; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.LinkedList; + +/** + * @author chen.luigi + */ +public class DebianParser extends AbstractParser { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(DebianParser.class); + private static final String PACKAGE = "Package"; + private static final String VERSION = "Version"; + private static final String ARCHITECTURE = "Architecture"; + private static final String DEBIAN_PACKAGE_PATTERN = "{0}_{1}_{2}.deb"; + + /* --- Overridden methods --- */ + + /** + * Parse the available file to create DependencyInfo + * Field to parse - Package, Version, Architecture, Filename, SystemPath, MD5sum + */ + @Override + public Collection parse(File file) { + BufferedReader br = null; + FileReader fr = null; + Collection dependencyInfos = new LinkedList<>(); + try { + fr = new FileReader(file.getAbsoluteFile()); + br = new BufferedReader(fr); + String line = null; + Package packageInfo = new Package(); + // Create Debian package - package-version-architecture.deb + while ((line = br.readLine()) != null) { + // Some fields (like 'Description') can be extended over several lines (which don't have ":") + // We don't need to parse these lines + if (!line.isEmpty() && line.contains(Constants.COLON)) { + String[] lineSplit = line.split(Constants.COLON); + // To prevent checking lines that look like - "Text:" + if (lineSplit.length > 1) { + String dependencyParameter = lineSplit[1].trim(); + switch (lineSplit[0]) { + case PACKAGE: + packageInfo.setPackageName(dependencyParameter); + break; + case VERSION: + if (packageInfo.getPackageName() != null) { + packageInfo.setVersion(dependencyParameter); + } + break; + case ARCHITECTURE: + if (packageInfo.getPackageName() != null) { + packageInfo.setArchitecture(dependencyParameter); + } + break; + default: + break; + } + } + } else { + if (packageInfo.getPackageName() != null) { + DependencyInfo dependencyInfo = createDependencyInfo(packageInfo); + packageInfo = new Package(); + dependencyInfos.add(dependencyInfo); + } + } + } + } catch (FileNotFoundException e) { + logger.error("Error getting package data {}", e.getMessage()); + } catch (IOException e) { + logger.error("Error getting package data {}", e.getMessage()); + } finally { + closeStream(br, fr); + } + return dependencyInfos; + } + + @Override + public File findFile(String[] files, String filename) { + for (String filepath : files) { + if (filepath.endsWith(filename)) { + return new File(filepath); + } + } + return null; + } + + /* --- Private methods --- */ + + private DependencyInfo createDependencyInfo(Package packageInfo) { + DependencyInfo dependencyInfo = null; + if (StringUtils.isNotBlank(packageInfo.getPackageName()) && StringUtils.isNotBlank(packageInfo.getVersion()) && + StringUtils.isNotBlank(packageInfo.getArchitecture())) { + if (packageInfo.getVersion().contains(Constants.PLUS)) { + dependencyInfo = new DependencyInfo( + null, MessageFormat.format(DEBIAN_PACKAGE_PATTERN, packageInfo.getPackageName(), + packageInfo.getVersion().substring(0, packageInfo.getVersion().lastIndexOf(Constants.PLUS)), packageInfo.getArchitecture()), packageInfo.getVersion()); + } else { + dependencyInfo = new DependencyInfo( + null, MessageFormat.format(DEBIAN_PACKAGE_PATTERN, packageInfo.getPackageName(), + packageInfo.getVersion(), packageInfo.getArchitecture()), packageInfo.getVersion()); + } + } + if (dependencyInfo != null) { + return dependencyInfo; + } else { + return null; + } + } + +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerImage.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerImage.java new file mode 100644 index 0000000..a84481a --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerImage.java @@ -0,0 +1,79 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import java.util.Objects; + +/** + * @author chen.luigi + */ +public class DockerImage { + + /* --- Members --- */ + + private String repository; + private String tag; + private String id; + + /* --- Constructors --- */ + + public DockerImage() { + } + + public DockerImage(String repository, String tag, String id) { + this.repository = repository; + this.tag = tag; + this.id = id; + } + + /* --- Overridden methods --- */ + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DockerImage)) return false; + DockerImage that = (DockerImage) o; + return Objects.equals(repository, that.repository) && + Objects.equals(tag, that.tag) && + Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + + return Objects.hash(repository, tag, id); + } + + @Override + public String toString() { + return "DockerImage{" + + "repository='" + repository + '\'' + + ", tag='" + tag + '\'' + + ", id='" + id + '\'' + + '}'; + } + + /* --- Getters / Setters --- */ + + public String getRepository() { + return repository; + } + + public void setRepository(String repository) { + this.repository = repository; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerResolver.java new file mode 100644 index 0000000..ff824c3 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/DockerResolver.java @@ -0,0 +1,527 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import org.apache.commons.collections.map.HashedMap; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.FileSystemScanner; +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.archive.ArchiveExtractor; +import org.whitesource.agent.dependency.resolver.docker.remotedocker.RemoteDockersManager; +import org.whitesource.agent.hash.FileExtensions; +import org.whitesource.agent.utils.FilesScanner; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.fs.FSAConfiguration; + +import java.io.*; +import java.text.MessageFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.whitesource.agent.Constants.*; +import static org.whitesource.agent.archive.ArchiveExtractor.TAR_SUFFIX; + +/** + * @author chen.luigi + */ +public class DockerResolver { + + /* --- Static members --- */ + + private static final Logger logger = LoggerFactory.getLogger(DockerResolver.class); + + private static final String TEMP_FOLDER = System.getProperty("java.io.tmpdir") + File.separator + TempFolders.UNIQUE_DOCKER_TEMP_FOLDER; + private static final String DOCKER_SAVE_IMAGE_COMMAND = "docker save"; + private static final String O_PARAMETER = "-o"; + private static final String REPOSITORY = "REPOSITORY"; + private static final String SPACES_REGEX = "\\s+"; + private static final String DOCKER_NAME_FORMAT_STRING = "{0} {1} ({2})"; + private static final MessageFormat DOCKER_NAME_FORMAT = new MessageFormat(DOCKER_NAME_FORMAT_STRING); + private static final String DOCKER_IMAGES = "docker images"; + private static final String DEBIAN_PATTERN = "**/*eipp.log.xz"; + private static final String ARCH_LINUX_PATTERN = "**/*desc"; + private static final String ALPINE_PATTERN = "**/*installed"; + private static final String DEBIAN_PATTERN_AVAILABLE = "**/*available"; + private static final String RPM_PATTERN = "**" + VAR + File.separator + LIB + File.separator + YUM + File.separator + YUM_DB + "/**"; + private static final String[] scanIncludes = {DEBIAN_PATTERN, ARCH_LINUX_PATTERN, ALPINE_PATTERN, RPM_PATTERN, DEBIAN_PATTERN_AVAILABLE}; + private static final String[] scanExcludes = {}; + private static final String ARCH_LINUX_DESC_FOLDERS = VAR + File.separator + LIB + File.separator + "pacman" + File.separator + "local"; + private static final String RPM_YUM_DB_FOLDER_DEFAULT_PATH = VAR + File.separator + LIB + File.separator + YUM + File.separator + YUM_DB; + private static final String DEBIAN_LIST_PACKAGES_FILE = File.separator + "eipp.log.xz"; + private static final String ALPINE_LIST_PACKAGES_FILE = File.separator + "installed"; + private static final String DEBIAN_LIST_PACKAGES_FILE_AVAILABLE = File.separator + "available"; + private static final String PACKAGE_LOG_TXT = "packageLog.txt"; + private static final boolean PARTIAL_SHA1_MATCH = false; + + /* --- Members --- */ + + private FSAConfiguration config; + private static Collection projects = new LinkedList<>(); + + /* --- Constructor --- */ + + public DockerResolver(FSAConfiguration config) { + this.config = config; + } + + /* --- Public methods --- */ + + /** + * Create project for each image + * + * @return list of projects for all docker images + */ + public Collection resolveDockerImages() { + + // TODO: Read this before changing RemoteDockersManager location + // Remote Docker pulling should be enable only if docker.scanImages==true && docker.pull.enable==true + // Before calling resolveDockerImages() there is a check for isScanDockerImages() + // If we create RemoteDockersManager outside of resolveDockerImages then we have to check isScanDockerImages() + RemoteDockersManager remoteDockersManager = new RemoteDockersManager(config.getRemoteDocker()); + remoteDockersManager.pullRemoteDockerImages(); + + String line = null; + Collection dockerImages = new LinkedList<>(); + Collection dockerImagesToScan; + Process process = null; + try { + // docker get list of images, use wait to get the whole list + boolean isTarImages = config.isScanImagesTar(); + if (!isTarImages) { + process = Runtime.getRuntime().exec(DOCKER_IMAGES); + InputStream inputStream = process.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); + logger.debug("Docker images list from BufferedReader"); + while ((line = br.readLine()) != null) { + logger.debug(line); + // read all docker images data, skip the first line + if (!line.startsWith(REPOSITORY)) { + String[] dockerImageString = line.split(SPACES_REGEX); + if (dockerImageString.length > 2) { + dockerImages.add(new DockerImage(dockerImageString[0], dockerImageString[1], dockerImageString[2])); + } else { + logger.info("Docker line content is ignored: {}", line); + } + } + } + process.waitFor(); + if (!dockerImages.isEmpty()) { + // filter docker images using includes & excludes parameter + dockerImagesToScan = filterDockerImagesToScan(dockerImages, config.getAgent().getDockerIncludes(), config.getAgent().getDockerExcludes()); + if (!dockerImagesToScan.isEmpty()) { + saveDockerImages(dockerImagesToScan, projects); + } + } + br.close(); + } else { + Collection tarFiles = new HashSet<>(); + FilenameFilter filenameFilter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if (name.endsWith(TAR_SUFFIX)) { + return true; + } + return false; + } + }; + for(String dir: config.getDependencyDirs()) { + File tarDir = new File(dir); + if (tarDir.isDirectory()) { + for (File tarFile: tarDir.listFiles(filenameFilter)) { + tarFiles.add(tarFile); + } + } + } + Collection tarImagesToScan = filterTarImagesToScan(tarFiles, config.getAgent().getDockerIncludes(), config.getAgent().getDockerExcludes()); + scanTarList(tarImagesToScan, projects); + } + } catch (IOException e) { + logger.error("IO exception : {}", e.getMessage()); + logger.debug("IO exception : {}", e.getStackTrace()); + } catch (InterruptedException e) { + logger.error("Interrupted exception : {}", e.getMessage()); + logger.debug("Interrupted exception : {}", e.getStackTrace()); + } catch (Exception e) { + logger.error("Exception : {}", e.getMessage()); + logger.debug("Resolve Docker Images Exception : {}", e); + } finally { + if (process != null) { + process.destroy(); + } + } + remoteDockersManager.removePulledRemoteDockerImages(); + return projects; + } + + /* --- Private methods --- */ + + /** + * Filter the images using includes and excludes lists + */ + private Collection filterDockerImagesToScan(Collection dockerImages, String[] dockerImageIncludes, String[] dockerImageExcludes) { + logger.info("Filtering docker images list by includes and excludes lists"); + Collection dockerImagesToScan = new LinkedList<>(); + Collection imageIncludesList = Arrays.asList(dockerImageIncludes); + Collection imageExcludesList = Arrays.asList(dockerImageExcludes); + for (DockerImage dockerImage : dockerImages) { + String dockerImageString = dockerImage.getRepository() + Constants.WHITESPACE + dockerImage.getTag() + Constants.WHITESPACE + dockerImage.getId(); + // add images to scan according to dockerIncludes pattern + if (isMatchingPattern(dockerImageString, imageIncludesList)) { + dockerImagesToScan.add(dockerImage); + } + // remove images from scan according to dockerExcludes pattern + if (isMatchingPattern(dockerImageString, imageExcludesList)) { + dockerImagesToScan.remove(dockerImage); + } + } + return dockerImagesToScan; + } + + private Collection filterTarImagesToScan(Collection dockerImages, String[] dockerImageIncludes, String[] dockerImageExcludes) { + logger.info("Filtering docker images list by includes and excludes lists"); + Collection dockerImagesToScan = new LinkedList<>(); + Collection imageIncludesList = Arrays.asList(dockerImageIncludes); + Collection imageExcludesList = Arrays.asList(dockerImageExcludes); + for (File dockerImage : dockerImages) { + String dockerImageString = dockerImage.getName(); + // add images to scan according to dockerIncludes pattern + if (isMatchingPattern(dockerImageString, imageIncludesList)) { + dockerImagesToScan.add(dockerImage); + } + // remove images from scan according to dockerExcludes pattern + if (isMatchingPattern(dockerImageString, imageExcludesList)) { + dockerImagesToScan.remove(dockerImage); + } + } + return dockerImagesToScan; + } + + private boolean isMatchingPattern(String dockerImageString, Collection imageIncludesList) { + for (String imageInclude : imageIncludesList) { + if (StringUtils.isNotBlank(imageInclude)) { + Pattern p = Pattern.compile(imageInclude); + Matcher m = p.matcher(dockerImageString); + if (m.find()) { + return true; + } + } + } + return false; + } + + /** + * Save docker images and scan files + */ + private void saveDockerImages(Collection dockerImages, Collection projects) throws IOException { + logger.info("Saving {} docker images", dockerImages.size()); + int counter = 1; + int imagesCount = dockerImages.size(); + for (DockerImage dockerImage : dockerImages) { + logger.info("Image {} of {} Images", counter, imagesCount); + //saveDockerImage(dockerImage, projects); + manageDockerImage(dockerImage, projects); + counter++; + } + } + + private void scanTarList (Collection tarFilesName, Collection projects) { + int i=0; + for (File tarFile:tarFilesName) { + String tar = tarFile.getAbsolutePath(); + i++; + logger.info("file {} : {}", i, tar); + } + for (File tarFile:tarFilesName) { + String tar = tarFile.getAbsolutePath(); + AgentProjectInfo projectInfo = new AgentProjectInfo(); + String tarName = tar.substring(tar.lastIndexOf(BACK_SLASH)+1); + String[] splitted = tarName.split(WHITESPACE); + if (splitted.length == 3) { + String id = splitted[0]; + String repository = splitted[1].replace(String.valueOf(SEMI_COLON), FORWARD_SLASH); + String tag = splitted[2].replace(String.valueOf(SEMI_COLON), FORWARD_SLASH) + .replace(String.valueOf(OPEN_BRACKET), EMPTY_STRING) + .replace(String.valueOf(CLOSE_BRACKET) + TAR_SUFFIX, EMPTY_STRING); + projectInfo.setCoordinates(new Coordinates(null, DOCKER_NAME_FORMAT.format(DOCKER_NAME_FORMAT_STRING, id, + repository, tag), null)); + projects.add(projectInfo); + File imageTarFile = new File(tar); + File imageExtractionDir = new File(TEMP_FOLDER, imageTarFile.getName()); + imageExtractionDir.mkdirs(); + extractAndBuildImage(imageTarFile, imageExtractionDir, projectInfo, config.deleteTarImages()); + scanImage(imageExtractionDir, projectInfo); + deleteDockerArchiveFiles(null, imageExtractionDir); + } else { + logger.info("file {} name is not in format 'Hash Name (Tag)'", tar); + } + } + } + + private void manageDockerImage(DockerImage dockerImage, Collection projects) throws IOException { + logger.debug("Saving image {} {}", dockerImage.getRepository(), dockerImage.getTag()); + // create agent project info + AgentProjectInfo projectInfo = new AgentProjectInfo(); + projectInfo.setCoordinates(new Coordinates(null, DOCKER_NAME_FORMAT.format(DOCKER_NAME_FORMAT_STRING, dockerImage.getId(), + dockerImage.getRepository(), dockerImage.getTag()), null)); + projects.add(projectInfo); + + File imageTarFile = new File(TEMP_FOLDER, dockerImage.getRepository() + TAR_SUFFIX); + File imageExtractionDir = new File(TEMP_FOLDER, dockerImage.getRepository()); + imageExtractionDir.mkdirs(); + + boolean saved = saveImage(dockerImage, imageTarFile); + if (saved) { + extractAndBuildImage(imageTarFile, imageExtractionDir, projectInfo, config.deleteTarImages()); + scanImage(imageExtractionDir, projectInfo); + } + + deleteDockerArchiveFiles(imageTarFile, imageExtractionDir); + } + + private boolean saveImage(DockerImage dockerImage, File imageTarFile) { + Process process = null; + try { + //Save image as tar file + process = Runtime.getRuntime().exec(DOCKER_SAVE_IMAGE_COMMAND + Constants.WHITESPACE + dockerImage.getId() + + Constants.WHITESPACE + O_PARAMETER + Constants.WHITESPACE + imageTarFile.getPath()); + process.waitFor(); + return true; + } catch (InterruptedException e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } catch (IOException e) { + logger.error("Error exporting image {}: {}", dockerImage.getRepository(), e.getMessage()); + logger.debug("Error exporting image {}", dockerImage.getRepository(), e); + } finally { + process.destroy(); + } + return false; + } + + private void extractAndBuildImage (File imageTarFile, File imageExtractionDir, AgentProjectInfo projectInfo, Boolean fromTarList) { + ArchiveExtractor archiveExtractor = new ArchiveExtractor(config.getAgent().getArchiveIncludes(), config.getAgent().getArchiveExcludes(), config.getAgent().getIncludes()); + try { + int megaByte = 1048576; // 1024*1024 + long tarSizeInBytes = imageTarFile.length(); + long tarSizeInMBs = tarSizeInBytes/megaByte; + long freeDiskSpaceInBytes = imageTarFile.getFreeSpace(); + long freeDiskSpaceInMBs = imageTarFile.getFreeSpace()/megaByte; + + logger.info("Extracting file {} - Size {} Bytes ({} MBs)- Free Space {} Bytes ({} MBs)", + imageTarFile.getCanonicalPath(), tarSizeInBytes, tarSizeInMBs, freeDiskSpaceInBytes, freeDiskSpaceInMBs); + } catch (Exception ex){ + logger.error("Could not get file size - {}", ex); + } + archiveExtractor.extractDockerImageLayers(imageTarFile, imageExtractionDir, fromTarList); + FilesScanner filesScanner = new FilesScanner(); + String[] fileNames = filesScanner.getDirectoryContent(imageExtractionDir.getParent(), scanIncludes, scanExcludes, true, false); + + // build the full path correctly + for (int i = 0; i < fileNames.length; i++) { + fileNames[i] = imageExtractionDir.getParent() + File.separator + fileNames[i]; + } + + // check for dependencies for each docker operating system (Debian,Arch-Linux,Alpine,Rpm) + AbstractParser parser = new DebianParser(); + File file = parser.findFile(fileNames, DEBIAN_LIST_PACKAGES_FILE); + + // extract .xz file to read the package log file + if (file != null) { + file = getPackagesLogFile(file, archiveExtractor); + } + parseProjectInfo(projectInfo, parser, file); + file = parser.findFile(fileNames, DEBIAN_LIST_PACKAGES_FILE_AVAILABLE); + if (file != null) { + parseProjectInfo(projectInfo, parser, file); + } + + // try to find duplicates and clear them + Collection debianDependencyInfos = mergeDependencyInfos(projectInfo); + if (debianDependencyInfos != null && !debianDependencyInfos.isEmpty()) { + projectInfo.getDependencies().clear(); + projectInfo.getDependencies().addAll(debianDependencyInfos); + } + logger.info("Found {} Debian Packages", debianDependencyInfos.size()); + + parser = new ArchLinuxParser(); + file = parser.findFile(fileNames, ARCH_LINUX_DESC_FOLDERS); + int archLinuxPackages = parseProjectInfo(projectInfo, parser, file); + logger.info("Found {} Arch linux Packages", archLinuxPackages); + + parser = new AlpineParser(); + file = parser.findFile(fileNames, ALPINE_LIST_PACKAGES_FILE); + int alpinePackages = parseProjectInfo(projectInfo, parser, file); + logger.info("Found {} Alpine Packages", alpinePackages); + + RpmParser rpmParser = new RpmParser(); + Collection yumDbFoldersPath = new LinkedList<>(); + RpmParser.findFolder(imageExtractionDir, YUM_DB, yumDbFoldersPath); + File yumDbFolder = rpmParser.checkFolders(yumDbFoldersPath, RPM_YUM_DB_FOLDER_DEFAULT_PATH); + int rpmPackages = parseProjectInfo(projectInfo, rpmParser, yumDbFolder); + logger.info("Found {} Rpm Packages", rpmPackages); + } + + private void scanImage (File imageExtractionDir, AgentProjectInfo projectInfo) { + String extractPath = imageExtractionDir.getPath(); + Set setDirs = new HashSet<>(); + setDirs.add(extractPath); + Map> appPathsToDependencyDirs = new HashMap<>(); + appPathsToDependencyDirs.put(FSAConfiguration.DEFAULT_KEY, setDirs); + List dependencyInfos = new FileSystemScanner(config.getResolver(), config.getAgent(), false).createProjects( + Arrays.asList(extractPath), appPathsToDependencyDirs, false, config.getAgent().getIncludes(), config.getAgent().getExcludes(), + config.getAgent().getGlobCaseSensitive(), config.getAgent().getArchiveExtractionDepth(), FileExtensions.ARCHIVE_INCLUDES, + FileExtensions.ARCHIVE_EXCLUDES, false, config.getAgent().isFollowSymlinks(), config.getAgent().getExcludedCopyrights(), PARTIAL_SHA1_MATCH, config.getAgent().getPythonRequirementsFileIncludes()); + + projectInfo.getDependencies().addAll(dependencyInfos); + } + + /* + private void saveDockerImage(DockerImage dockerImage, Collection projects) throws IOException { + logger.debug("Saving image {} {}", dockerImage.getRepository(), dockerImage.getTag()); + Process process = null; + // create agent project info + AgentProjectInfo projectInfo = new AgentProjectInfo(); + projectInfo.setCoordinates(new Coordinates(null, DOCKER_NAME_FORMAT.format(DOCKER_NAME_FORMAT_STRING, dockerImage.getId(), + dockerImage.getRepository(), dockerImage.getTag()), null)); + projects.add(projectInfo); + + File imageTarFile = new File(TEMP_FOLDER, dockerImage.getRepository() + TAR_SUFFIX); + File imageExtractionDir = new File(TEMP_FOLDER, dockerImage.getRepository()); + imageExtractionDir.mkdirs(); + try { + //Save image as tar file + process = Runtime.getRuntime().exec(DOCKER_SAVE_IMAGE_COMMAND + Constants.WHITESPACE + dockerImage.getId() + + Constants.WHITESPACE + O_PARAMETER + Constants.WHITESPACE + imageTarFile.getPath()); + process.waitFor(); + + // extract tar archive + ArchiveExtractor archiveExtractor = new ArchiveExtractor(config.getAgent().getArchiveIncludes(), config.getAgent().getArchiveExcludes(), config.getAgent().getIncludes()); + try { + int megaByte = 1048576; // 1024*1024 + long tarSizeInBytes = imageTarFile.length(); + long tarSizeInMBs = tarSizeInBytes / megaByte; + long freeDiskSpaceInBytes = imageTarFile.getFreeSpace(); + long freeDiskSpaceInMBs = imageTarFile.getFreeSpace() / megaByte; + + logger.info("Extracting file {} - Size {} Bytes ({} MBs)- Free Space {} Bytes ({} MBs)", + imageTarFile.getCanonicalPath(), tarSizeInBytes, tarSizeInMBs, freeDiskSpaceInBytes, freeDiskSpaceInMBs); + } catch (Exception ex) { + logger.error("Could not get file size - {}", ex); + } + archiveExtractor.extractDockerImageLayers(imageTarFile, imageExtractionDir, false); + FilesScanner filesScanner = new FilesScanner(); + String[] fileNames = filesScanner.getDirectoryContent(imageExtractionDir.getParent(), scanIncludes, scanExcludes, true, false); + + // build the full path correctly + for (int i = 0; i < fileNames.length; i++) { + fileNames[i] = imageExtractionDir.getParent() + File.separator + fileNames[i]; + } + + // check for dependencies for each docker operating system (Debian,Arch-Linux,Alpine,Rpm) + AbstractParser parser = new DebianParser(); + File file = parser.findFile(fileNames, DEBIAN_LIST_PACKAGES_FILE); + + // extract .xz file to read the package log file + if (file != null) { + file = getPackagesLogFile(file, archiveExtractor); + } + parseProjectInfo(projectInfo, parser, file); + file = parser.findFile(fileNames, DEBIAN_LIST_PACKAGES_FILE_AVAILABLE); + if (file != null) { + parseProjectInfo(projectInfo, parser, file); + } + + // try to find duplicates and clear them + Collection debianDependencyInfos = mergeDependencyInfos(projectInfo); + if (debianDependencyInfos != null && !debianDependencyInfos.isEmpty()) { + projectInfo.getDependencies().clear(); + projectInfo.getDependencies().addAll(debianDependencyInfos); + } + logger.info("Found {} Debian Packages", debianDependencyInfos.size()); + + parser = new ArchLinuxParser(); + file = parser.findFile(fileNames, ARCH_LINUX_DESC_FOLDERS); + int archLinuxPackages = parseProjectInfo(projectInfo, parser, file); + logger.info("Found {} Arch linux Packages", archLinuxPackages); + + parser = new AlpineParser(); + file = parser.findFile(fileNames, ALPINE_LIST_PACKAGES_FILE); + int alpinePackages = parseProjectInfo(projectInfo, parser, file); + logger.info("Found {} Alpine Packages", alpinePackages); + + RpmParser rpmParser = new RpmParser(); + Collection yumDbFoldersPath = new LinkedList<>(); + RpmParser.findFolder(imageExtractionDir, YUM_DB, yumDbFoldersPath); + File yumDbFolder = rpmParser.checkFolders(yumDbFoldersPath, RPM_YUM_DB_FOLDER_DEFAULT_PATH); + int rpmPackages = parseProjectInfo(projectInfo, rpmParser, yumDbFolder); + logger.info("Found {} Rpm Packages", rpmPackages); + + // scan files + String extractPath = imageExtractionDir.getPath(); + Set setDirs = new HashSet<>(); + setDirs.add(extractPath); + Map> appPathsToDependencyDirs = new HashMap<>(); + appPathsToDependencyDirs.put(FSAConfiguration.DEFAULT_KEY, setDirs); + ProjectConfiguration projectConfiguration = new ProjectConfiguration(config.getAgent(), Arrays.asList(extractPath), appPathsToDependencyDirs, false); + Collection agentProjectInfos = new FileSystemScanner(config.getResolver(), config.getAgent(), false).createProjects(projectConfiguration).keySet(); + List dependencyInfos = agentProjectInfos.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()); + + projectInfo.getDependencies().addAll(dependencyInfos); + } catch (IOException e) { + logger.error("Error exporting image {}: {}", dockerImage.getRepository(), e.getMessage()); + logger.debug("Error exporting image {}", dockerImage.getRepository(), e); + } catch (ArchiverException e) { + logger.error("Error extracting {}: {}", imageTarFile, e.getMessage()); + logger.debug("Error extracting tar archive", e); + } catch (InterruptedException e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } finally { + process.destroy(); + deleteDockerArchiveFiles(imageTarFile, imageExtractionDir); + } + }*/ + + private Collection mergeDependencyInfos(AgentProjectInfo projectInfo) { + Map infoMap = new HashedMap(); + Collection dependencyInfos = new LinkedList<>(); + if (projectInfo != null) { + Collection dependencies = projectInfo.getDependencies(); + for (DependencyInfo dependencyInfo : dependencies) { + infoMap.putIfAbsent(dependencyInfo.getArtifactId(), dependencyInfo); + } + } + for (Map.Entry entry : infoMap.entrySet()) { + if (entry.getValue() != null) { + dependencyInfos.add(entry.getValue()); + } + } + return dependencyInfos; + } + + private File getPackagesLogFile(File file, ArchiveExtractor archiveExtractor) { + archiveExtractor.unXz(file, file.getParent() + File.separator + PACKAGE_LOG_TXT); + return new File(file.getParent() + File.separator + PACKAGE_LOG_TXT); + } + + private int parseProjectInfo(AgentProjectInfo projectInfo, AbstractParser parser, File file) { + if (file != null) { + Collection packageManagerPackages = parser.parse(file); + if (!packageManagerPackages.isEmpty()) { + projectInfo.getDependencies().addAll(packageManagerPackages); + } + return packageManagerPackages.size(); + } + return 0; + } + + private void deleteDockerArchiveFiles(File imageTarFile, File imageTarExtractDir) { + FileUtils.deleteQuietly(imageTarFile); + FileUtils.deleteQuietly(imageTarExtractDir); + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/Package.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/Package.java new file mode 100644 index 0000000..91b4341 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/Package.java @@ -0,0 +1,71 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import java.util.Objects; + +/** + * @author chen.luigi + */ +public class Package { + + /* --- Private members --- */ + + private String packageName; + private String version; + private String architecture; + + /* --- Constructors --- */ + + public Package() { + } + + public Package(String packageName, String version, String architecture) { + this.packageName = packageName; + this.version = version; + this.architecture = architecture; + } + + /* --- Overridden methods --- */ + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Package)) return false; + Package aPackage = (Package) o; + return Objects.equals(packageName, aPackage.packageName) && + Objects.equals(version, aPackage.version) && + Objects.equals(architecture, aPackage.architecture); + } + + @Override + public int hashCode() { + + return Objects.hash(packageName, version, architecture); + } + + /* --- Public methods --- */ + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getArchitecture() { + return architecture; + } + + public void setArchitecture(String architecture) { + this.architecture = architecture; + } + +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/RpmParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/RpmParser.java new file mode 100644 index 0000000..2694e70 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/RpmParser.java @@ -0,0 +1,98 @@ +package org.whitesource.agent.dependency.resolver.docker; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.LinkedList; + +/** + * @author chen.luigi + */ +public class RpmParser extends AbstractParser { + + /* --- Static members --- */ + + private static final Logger logger = LoggerFactory.getLogger(RpmParser.class); + private static final String RPM_PACKAGE_PATTERN = "{0}.rpm"; + + /* --- Overridden methods --- */ + + @Override + public Collection parse(File file) { + BufferedReader br = null; + FileReader fr = null; + Collection dependencyInfos = new LinkedList<>(); + try { + File[] files = file.listFiles(); + for (File directory : files) { + File[] packageDirectories = directory.listFiles(); + for (File packageDirectory : packageDirectories) { + // parse the package name from package directory + // get the start index of package name + // package Directory Name for + int firstHyphenIndex = packageDirectory.getName().indexOf(Constants.DASH); + String packageInfoString = packageDirectory.getName().substring(firstHyphenIndex + 1, packageDirectory.getName().length()); + // change rpm pattern name to application pattern + int lastIndexOfHyphen = packageInfoString.lastIndexOf(Constants.DASH); + packageInfoString = packageInfoString.substring(0, lastIndexOfHyphen) + Constants.DOT + packageInfoString.substring(lastIndexOfHyphen + 1); + // create dependencyInfo object + DependencyInfo dependencyInfo = null; + String packVersion = getPackageVersion(packageInfoString); + if (packVersion != null) { + dependencyInfo = new DependencyInfo( + null, MessageFormat.format(RPM_PACKAGE_PATTERN, packageInfoString), packVersion); + dependencyInfos.add(dependencyInfo); + } + } + } + } catch (Exception e) { + logger.warn("Failed to parse {} : {}", file, e.getMessage()); + } finally { + closeStream(br, fr); + } + return dependencyInfos; + } + + // get rpm package version + private String getPackageVersion(String packageInfoString) { + // packageInfoString for example - audit-libs-2.7.6-3.el7-x86_64 + try { + String firstDotString = packageInfoString.substring(0, packageInfoString.indexOf(Constants.DOT)); + int lastIndexOfHyphen = firstDotString.lastIndexOf(Constants.DASH); + int lastIndexOfDot = packageInfoString.lastIndexOf(Constants.DOT); + String packVersion = packageInfoString.substring(lastIndexOfHyphen + 1, lastIndexOfDot); + if (StringUtils.isNotBlank(packVersion)) { + return packVersion; + } + } catch (Exception e) { + logger.warn("Failed to create package version : {}", e.getMessage()); + } + return null; + } + + @Override + public File findFile(String[] files, String filename) { + return null; + } + + // find yumdb folder from collection + public File checkFolders(Collection yumDbFolders, String yumDbFolderPath) { + if (!yumDbFolders.isEmpty()) { + for (String folderPath : yumDbFolders) { + File file = new File(folderPath); + if (file.listFiles().length > 0 && folderPath.contains(yumDbFolderPath)) { + return file; + } + } + } + return null; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDocker.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDocker.java new file mode 100644 index 0000000..30c6669 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDocker.java @@ -0,0 +1,389 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.Pair; +import org.whitesource.fs.configuration.RemoteDockerConfiguration; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class AbstractRemoteDocker { + + /* --- Static members --- */ + + private static final Logger logger = LoggerFactory.getLogger(AbstractRemoteDocker.class); + protected static final String DOCKER_CLI_VERSION = "docker --version"; + protected static final String DOCKER_CLI_LOGIN = "docker login"; + protected static final String DOCKER_CLI_REMOVE_IMAGE = "docker rmi "; + protected static final String DOCKER_CLI_PULL = "docker pull "; + protected static final String LINUX_PREFIX_SUDO = "sudo "; + protected static final String WS_SCANNED_TAG = "WS.Scanned"; + + // This is a set of the pulled images only - Users may require to pull existing images - but they are not saved + // in this set because we will remove the images that we pulled here (we don't want to remove the existing images + // of the users) + private Set imagesPulled; + + private Set imagesFound; + + protected RemoteDockerConfiguration config; + + private int pulledImagesCount; + private int existingImagesCount; + private int maxScanImagesCount; + private int scannedImagesCount; + + /* --- Constructors --- */ + + public AbstractRemoteDocker(RemoteDockerConfiguration config) { + this.config = config; + pulledImagesCount = 0; + existingImagesCount = 0; + maxScanImagesCount = config.getMaxScanImages(); + scannedImagesCount = 0; + } + + /* --- Public methods --- */ + + public Set pullRemoteDockerImages() { + if (isAllSoftwareRequiredInstalled()) { + if (loginToRemoteRegistry()) { + imagesFound = getRemoteRegistryImagesList(); + if (imagesFound != null && !imagesFound.isEmpty()) { + imagesPulled = pullImagesFromRemoteRegistry(); + } + logger.info("{} New images were pulled", pulledImagesCount); + logger.info("{} Images are up to date (not pulled)", existingImagesCount); + + // Logout from account if UA logged in + logoutRemoteDocker(); + } + } + return imagesPulled; + } + + public void removePulledRemoteDockerImages() { + if (imagesPulled != null && !imagesPulled.isEmpty()) { + logger.info("Remove pulled remote docker images"); + for(AbstractRemoteDockerImage image: imagesPulled) { + String command = DOCKER_CLI_REMOVE_IMAGE; + // Use force delete + if (config.isForceDelete()) { + command += "-f "; + } + Pair result = executeCommand(command + image.getUniqueIdentifier()); + if(result.getKey() == 0) { + logger.debug("Image '{}' removed successfully.", image.getRepositoryName()); + } else { + logger.debug("Image '{}' wasn't removed.", image.getRepositoryName()); + } + } + } + } + + protected abstract boolean loginToRemoteRegistry(); + + protected abstract void logoutRemoteDocker(); + + /* + TODO: this function should return a collection of manifest image object + TODO: DockerImage object does not include all the required data (like date, tags list, etc...) + */ + protected abstract Set getRemoteRegistryImagesList(); + + protected abstract boolean isRegistryCliInstalled(); + + protected abstract String getImageFullURL(AbstractRemoteDockerImage image); + + private Set pullImagesFromRemoteRegistry() { + Set pulledImagesList = new HashSet<>(); + int maxPullImages = config.getMaxPullImages(); + int pullImagesCounter = 0; + if (maxPullImages < 1) { + logger.info("No images will be pull - Configuration 'docker.pull.maxImages' is equal to {} ", maxPullImages); + return pulledImagesList; + } + for (AbstractRemoteDockerImage image : imagesFound) { + // Check if image meets the required name/tag/digest + if (isImagePullRequired(image)) { + String imageURL = getImageFullURL(image); + if (pullImageWithFullUrl(imageURL)) { + pulledImagesList.add(image); + pullImagesCounter++; + } + } + if (pullImagesCounter >= maxPullImages) { + logger.info("Reached maximum images pull count of {} - will not pull any more images", pullImagesCounter); + break; + } + } + return pulledImagesList; + } + + private boolean isAllSoftwareRequiredInstalled() { + return isDockerInstalled() && isRegistryCliInstalled(); + } + + private boolean isAllNamesRequired() { + List namesList = config.getImageNames(); + boolean allNames = false; + // If images list is not configured - we assume that we want to scan ALL images + if (namesList == null || namesList.isEmpty() || namesList.contains(Constants.GLOB_PATTERN)) { + allNames = true; + } + return allNames; + } + + private boolean isAllTagsRequired() { + List tagsList = config.getImageTags(); + boolean allTags = false; + // If tag list is not configured - we assume that we want to scan ALL tags + if (tagsList == null || tagsList.isEmpty() || tagsList.contains(Constants.GLOB_PATTERN)) { + allTags = true; + } + return allTags; + } + + private boolean isAllDigestsRequired() { + List digestList = config.getImageDigests(); + boolean allDigests = false; + // If digest list is not configured - we assume that we want to scan ALL tags + if (digestList == null || digestList.isEmpty() || digestList.contains(Constants.GLOB_PATTERN)) { + allDigests = true; + } + return allDigests; + } + + private boolean isAllImagesRequired() { + // forcePulling = Pull images that are already scanned + boolean forcePulling = config.isForcePull(); + // We want to pull everything - so every image is required + if (isAllNamesRequired() && isAllTagsRequired() && isAllDigestsRequired() && forcePulling) { + return true; + } + return false; + } + + private boolean isImageDataValid(AbstractRemoteDockerImage image) { + List imageTags = image.getImageTags(); + String imageDigest = image.getImageDigest(); + String imageName = image.getRepositoryName(); + + if (imageTags == null && StringUtils.isBlank(imageDigest) && StringUtils.isBlank(imageName)) { + // This tag/sha256/name does not met any of the requirements + logger.debug("Image values are empty or null"); + return false; + } + return true; + } + + // TODO: this function should receive manifest image object and check for all values (like date, tags list, etc.) + private boolean isImagePullRequired(AbstractRemoteDockerImage image) { + boolean isAllNamesRequired = isAllNamesRequired(); + boolean isAllTagsRequired = isAllTagsRequired(); + boolean isAllDigestsRequired = isAllDigestsRequired(); + + // All images are required ? + boolean forcePulling = config.isForcePull(); + // We want to pull everything - so every image is required + if (isAllNamesRequired && isAllTagsRequired && isAllDigestsRequired && forcePulling) { + return true; + } + + // Otherwise we check if the image data is full for specific tag(s)/sha256 + if (!isImageDataValid(image)) { + return false; + } + + // Data from the remote image + List imageTags = image.getImageTags(); + String imageDigest = image.getImageDigest(); + String imageName = image.getRepositoryName(); + + if (imageTags == null || imageTags.isEmpty()) { + logger.info("Image {} with Digest {} - does not have any tags and will be ignored", imageName, imageDigest); + return false; + } + + // Data from the config file + List configImageNames = config.getImageNames(); + List configImageTags = config.getImageTags(); + List configImageDigests = config.getImageDigests(); + + boolean isNameMet; + // Empty configuration = similar to using all names + if (configImageNames == null) { + isNameMet = true; + logger.debug("Configuration - docker.pull.images was not found - using .*.* as default"); + } + // 'Name' may have a regular expression match + else { + isNameMet = isAllNamesRequired || configImageNames.contains(imageName) || isMatchStringInList(imageName, configImageNames); + } + + // 'Tag' may have a regular expression match + // 'Tag' should consider pulling/ignoring images with the WS.Scanned tag - according to 'docker.pull.force' flag + boolean isTagMet = false; + if (configImageTags == null) { + isTagMet = true; + logger.debug("Configuration - docker.pull.tags was not found - using .*.* as default"); + }else { + for(String currentTag : imageTags) { + isTagMet = isTagMet || configImageTags.contains(currentTag) || isMatchStringInList(currentTag, configImageTags); + } + } + + // 'Digest' cannot have a regular expression match + boolean isDigestMet = isAllDigestsRequired || configImageDigests.contains(imageDigest); + + // Do not pull images that are already tagged with WS.Scanned tag + if (!forcePulling) { + if (imageTags.contains(WS_SCANNED_TAG)) { + logger.debug("Image {} - was scanned already", imageName); + isTagMet = false; + } + } + + // Is this tag/sha256/name in the required tags/sha256/name list? + boolean result = isNameMet && isTagMet && isDigestMet ; + if (!result) { + logger.debug("Image does not met the requirements: {}", image); + logger.debug("Name met - {} , Tag met - {} , Digest met - {}", isNameMet, isTagMet, isDigestMet); + } + return result; + } + + private boolean pullImageWithFullUrl(String imageURL) { + boolean result = false; + if (!StringUtils.isBlank(imageURL)) { + logger.info("Trying to pull image : {}", imageURL); + String command = DOCKER_CLI_PULL; + command += imageURL; + try { + // TODO: check if can use CommandLineProcess + Process process = Runtime.getRuntime().exec(command); + StringBuilder resultText = new StringBuilder(); + try (final BufferedReader reader + = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + System.out.println(line); + resultText.append(line); + } + } + int resultValue = process.waitFor(); + result = resultValue == 0; + if (result) { + int index = resultText.indexOf("Status:"); + if (index > 0) { + String status = resultText.substring(index); + logger.info("{}", status); + if (status.contains("Image is up to date for")) { + existingImagesCount++; + result = false; // The image was not pulled + } else if (status.contains("Downloaded newer image for")) { + pulledImagesCount++; + } + } + } else { + logger.info("Image was not pulled!"); + logger.info("{}", resultText); + } + } catch (InterruptedException e) { + logger.info("Execution of {} failed: - {}", command, e.getMessage()); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.info("Execution of {} failed: - {}", command, e.getMessage()); + } + } + return result; + } + + // This function cannot be run with multiple threads because each time it is run it overrides the values + // of resultVal and inputStream, so if other thread (for example) is reading the stream, it might get mixed data + // from old command's stream and the new command's stream + protected Pair executeCommand(String command) { + int resultVal = 1; + InputStream inputStream = null; + try { + logger.debug("Executing command: {}", command); + Process process = Runtime.getRuntime().exec(command); + + resultVal = process.waitFor(); + inputStream = process.getInputStream(); + + } catch (InterruptedException e) { + logger.info("Execution of {} failed: code - {} ; message - {}", command, resultVal, e.getMessage()); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.info("Execution of {} failed: code - {} ; message - {}", command, resultVal, e.getMessage()); + } + if (inputStream == null) { + // Create an empty InputStream instead of returning a null + inputStream = new InputStream() { + @Override + public int read() throws IOException { + return -1; + } + } ; + } + return new Pair<>(resultVal, inputStream); + } + + protected boolean isCommandSuccessful(String command) { + Pair result = executeCommand(command); + Integer val = result.getKey(); + return val == 0; + } + + private boolean isDockerInstalled() { + String command = DOCKER_CLI_VERSION; + boolean installed = isCommandSuccessful(command); + if (!installed) { + logger.error("Docker is not installed or its path is not configured correctly"); + } + return installed; + } + + private boolean isMatchStringInList(String toMatch, List stringsList) { + if (toMatch == null || stringsList == null || stringsList.isEmpty()) { + return false; + } + for (String currentString : stringsList) { + if (toMatch.matches(currentString)) { + return true; + } + } + return false; + } + + /* + Get image sha256 extracted from digest from image manifest. + */ + protected String getSHA256FromManifest(String manifest) { + if (StringUtils.isBlank(manifest)) { + return Constants.EMPTY_STRING; + } + + // sha256 regex, matched group will return string contains digits and chars, String length 64 + String sha256Regex = "sha256:([\\w\\d]{64})"; + Pattern pattern = Pattern.compile(sha256Regex); + Matcher matcher = pattern.matcher(manifest); + + if(matcher.find()){ + return matcher.group(1); + } else { + logger.error("Could not get config -> digest -> sha256 value from manifest"); + logger.error("Manifest content - {}", manifest); + return Constants.EMPTY_STRING; + } + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDockerImage.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDockerImage.java new file mode 100644 index 0000000..6c5400d --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/AbstractRemoteDockerImage.java @@ -0,0 +1,83 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker; + +import org.whitesource.agent.dependency.resolver.docker.DockerImage; + +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +public abstract class AbstractRemoteDockerImage { + + protected String repositoryName; + protected String imageDigest; + protected String imageSha256; + protected List imageTags; + protected Date imagePushedAt; + // Contains SHA256 similar to Docker's SHA256 + protected String imageManifest; + + public AbstractRemoteDockerImage() { + } + + public AbstractRemoteDockerImage(String repositoryName, String imageDigest, List imageTags, + Date imagePushedAt, String imageManifest, String imageSha256) { + this.repositoryName = repositoryName; + this.imageDigest = imageDigest; + this.imageTags = imageTags; + this.imagePushedAt = imagePushedAt; + this.imageManifest = imageManifest; + this.imageSha256 = imageSha256; + } + + public String getRepositoryName() { + return repositoryName; + } + + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + public String getImageDigest() { + return imageDigest; + } + + public String getImageSha256() { + return imageSha256; + } + + public void setImageSha256(String imageSha256) { + this.imageSha256 = imageSha256; + } + + public void setImageDigest(String imageDigest) { + this.imageDigest = imageDigest; + } + + public List getImageTags() { + return imageTags; + } + + public void setImageTags(List imageTags) { + this.imageTags = imageTags; + } + + public DockerImage getDockerImage(String tag) { + return new DockerImage(repositoryName, tag, imageDigest); + } + + public List getAllDockerImages() { + if (imageTags == null || imageTags.isEmpty()) { + return Collections.emptyList(); + } + List result = new LinkedList<>(); + for(String tag : imageTags) { + result.add(getDockerImage(tag)); + } + return result; + } + + /* --- Abstract methods --- */ + + public abstract String getUniqueIdentifier(); +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/RemoteDockersManager.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/RemoteDockersManager.java new file mode 100644 index 0000000..23468c3 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/RemoteDockersManager.java @@ -0,0 +1,56 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker; + +import org.whitesource.agent.dependency.resolver.docker.remotedocker.amazon.RemoteDockerAmazonECR; +import org.whitesource.agent.dependency.resolver.docker.remotedocker.azure.AzureRemoteDocker; +import org.whitesource.fs.configuration.RemoteDockerConfiguration; + +import java.util.*; + +public class RemoteDockersManager { + + private boolean remoteDockersEnabled = false; + private List remoteDockersList = new LinkedList<>(); + private Set pulledDockerImages = new HashSet<>(); + + public RemoteDockersManager(RemoteDockerConfiguration config) { + if (config != null) { + remoteDockersEnabled = config.isRemoteDockerEnabled(); + // TODO: Remote Docker pulling should be enable only if docker.scanImages==true && docker.pull.enable==true + if (remoteDockersEnabled) { + if (config.isRemoteDockerAmazonEnabled()) { + remoteDockersList.add(new RemoteDockerAmazonECR(config)); + } + if(config.isRemoteDockerAzureEnabled()) { + remoteDockersList.add(new AzureRemoteDocker(config)); + } + } + } + } + + public Set pullRemoteDockerImages() { + if (!remoteDockersEnabled) { + return Collections.emptySet(); + } + for (AbstractRemoteDocker remoteDocker : remoteDockersList) { + Set pulledImages = remoteDocker.pullRemoteDockerImages(); + if (pulledImages != null) { + pulledDockerImages.addAll(pulledImages); + } + } + return pulledDockerImages; + } + + public void removePulledRemoteDockerImages() { + if (!remoteDockersEnabled) { + return; + } + for (AbstractRemoteDocker remoteDocker : remoteDockersList) { + remoteDocker.removePulledRemoteDockerImages(); + } + pulledDockerImages.clear(); + } + + public Set getPulledDockerImages() { + return pulledDockerImages; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/DockerImageAmazon.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/DockerImageAmazon.java new file mode 100644 index 0000000..16d31e3 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/DockerImageAmazon.java @@ -0,0 +1,59 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker.amazon; + +import org.whitesource.agent.dependency.resolver.docker.remotedocker.AbstractRemoteDockerImage; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +public class DockerImageAmazon extends AbstractRemoteDockerImage { + + /* --- Private members --- */ + + private String registryId; + private String mainTag; + + /* --- Constructors --- */ + + public DockerImageAmazon(String registryId, String repositoryName, String imageDigest, List imageTags, + Date imagePushedAt, String imageManifest, String mainTag, String imageSha256) { + super(repositoryName, imageDigest, imageTags, imagePushedAt, imageManifest, imageSha256); + this.registryId = registryId; + this.mainTag = mainTag; + } + + /* --- Overridden public methods --- */ + + @Override + public String getUniqueIdentifier() { + return this.getImageSha256(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DockerImageAmazon that = (DockerImageAmazon) o; + return Objects.equals(registryId, that.registryId) && + Objects.equals(mainTag, that.mainTag); + } + + @Override + public int hashCode() { + + return Objects.hash(registryId, mainTag); + } + + @Override + public String toString() { + return "DockerImageAmazon{" + + "registryId='" + registryId + '\'' + + ", mainTag='" + mainTag + '\'' + + ", repositoryName='" + repositoryName + '\'' + + ", imageDigest='" + imageDigest + '\'' + + ", imageTags=" + imageTags + + ", imagePushedAt=" + imagePushedAt + + ", imageManifest='" + imageManifest + '\'' + + '}'; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/RemoteDockerAmazonECR.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/RemoteDockerAmazonECR.java new file mode 100644 index 0000000..3303662 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/amazon/RemoteDockerAmazonECR.java @@ -0,0 +1,366 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker.amazon; + +import com.amazonaws.services.ecr.AmazonECR; +import com.amazonaws.services.ecr.AmazonECRClientBuilder; +import com.amazonaws.services.ecr.model.*; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.SystemUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.docker.DockerImage; +import org.whitesource.agent.dependency.resolver.docker.remotedocker.AbstractRemoteDocker; +import org.whitesource.agent.dependency.resolver.docker.remotedocker.AbstractRemoteDockerImage; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.Pair; +import org.whitesource.fs.configuration.RemoteDockerConfiguration; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.*; + +public class RemoteDockerAmazonECR extends AbstractRemoteDocker { + + private static final Logger logger = LoggerFactory.getLogger(AbstractRemoteDocker.class); + private static final String AWS_VERSION = "aws --version"; + private static final String AWS_ECR_GET_LOGIN = "aws ecr get-login --no-include-email"; + private static final AmazonECR amazonClient = AmazonECRClientBuilder.standard().build(); + + private Map imageToRepositoryUriMap; + private Map imageDigestToTagMap; + private String defaultRegistryId; + + public RemoteDockerAmazonECR(RemoteDockerConfiguration config) { + super(config); + imageToRepositoryUriMap = new HashMap<>(); + imageDigestToTagMap = new HashMap<>(); + } + + public boolean isRegistryCliInstalled() { + boolean installed = isCommandSuccessful(AWS_VERSION); + if (!installed) { + logger.error("AWS ECR is not installed or its path is not configured correctly"); + } + return installed; + } + + public boolean loginToRemoteRegistry() { + boolean saveDefaultRegistryId = true; + StringBuilder stCommand = new StringBuilder(); + stCommand.append(AWS_ECR_GET_LOGIN); + if (config != null) { + List registriesList = config.getAmazonRegistryIds(); + // We have several registry Ids + if (registriesList.size() > 1) { + stCommand.append(Constants.WHITESPACE); + stCommand.append("--registry-ids"); + for (String registryId : registriesList) { + stCommand.append(Constants.WHITESPACE); + stCommand.append(registryId); + } + // We have several registry ids - so we don't know which is the default + saveDefaultRegistryId = false; + } else if (registriesList.size() == 1) { + String registryId = registriesList.get(0); + if (Constants.EMPTY_STRING.equals(registryId)) { + logger.info("No registryIds value is found! Logging to default Amazon ECR registry"); + } else { + stCommand.append(Constants.WHITESPACE); + stCommand.append("--registry-ids"); + stCommand.append(Constants.WHITESPACE); + stCommand.append(registryId); + } + } + } + + boolean loginResult = false; + // Run this command to ask for permissions from Amazon to run Docker + Pair result = executeCommand(stCommand.toString()); + Integer intVal = result.getKey(); + // The request was successful + if (intVal == 0) { + try { + String line; + // The result will be a long string with the full information (like password, region, etc) + InputStream inputStream = result.getValue(); + BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); + + // Each line will include a Docker login command + while ((line = br.readLine()) != null) { + if (line.startsWith(DOCKER_CLI_LOGIN)) { + String command = line; + if (SystemUtils.IS_OS_LINUX && config.isLoginSudo()) { + command = LINUX_PREFIX_SUDO + command; + } + // Execute the Docker command and get permission + result = executeCommand(command); + boolean loginToCurrentRegistry = (result.getKey() == 0); + // Were able to login to at least 1 registry + loginResult = loginResult || loginToCurrentRegistry; + String[] dockerCommandVal = line.split(Constants.WHITESPACE); + if (dockerCommandVal.length > 1) { + String registryPath = dockerCommandVal[dockerCommandVal.length - 1]; + String loginMessage = "OK"; + if (!loginToCurrentRegistry) { + loginMessage = "Failed"; + } + logger.info("Login to registry : {} - {}", registryPath, loginMessage); + if (saveDefaultRegistryId) { + defaultRegistryId = registryPath; + } + } + else { + logger.info("Invalid Docker login command: {}", line); + } + } + } + } catch (IOException e) { + logger.info("Execution of {} failed - {}", AWS_ECR_GET_LOGIN, e.getMessage()); + } + } else { + logger.info("Login to registries list - {} - failed", config.getAmazonRegistryIds()); + logger.debug("loginToRemoteRegistry - failed with error code {}", intVal); + } + return loginResult; + } + + @Override + public String getImageFullURL(AbstractRemoteDockerImage image) { + String result = Constants.EMPTY_STRING; + if (image != null) { + String repositoryUri = imageToRepositoryUriMap.get(image.getRepositoryName()); + if (repositoryUri != null && !repositoryUri.isEmpty()) { + /* Command should look like: + 'docker pull {registryId}.dkr.ecr.us-east-1.amazonaws.com/{imageName}:{Tag}' + */ + result = repositoryUri + Constants.COLON + imageDigestToTagMap.get(image.getImageDigest()); + //result = repositoryUri + "@" + image.getImageDigest(); + } + } + return result; + } + + private Collection getRepositoriesList(String registryId, List repositoryNames) { + // aws ecr describe-repositories [--registry-id ] [--repository-names ] + + logger.debug("getRepositoriesList start"); + // If registry id is null/empty - the default registry is assumed + DescribeRepositoriesRequest request = new DescribeRepositoriesRequest(); + if (!StringUtils.isBlank(registryId)) { + request = request.withRegistryId(registryId); + logger.debug("getRepositoriesList - registryId= {}", registryId); + } + // If repository names is null/empty - then all repositories in a registry are described + if (repositoryNames != null && !repositoryNames.isEmpty()) { + request = request.withRepositoryNames(repositoryNames); + logger.debug("getRepositoriesList - repositoryNames= {}", repositoryNames); + } + + Collection repositoriesList = Collections.emptyList(); + try { + DescribeRepositoriesResult response = amazonClient.describeRepositories(request); + repositoriesList = response.getRepositories(); + if (repositoriesList != null) { + for (Repository repository : repositoriesList) { + imageToRepositoryUriMap.put(repository.getRepositoryName(),repository.getRepositoryUri()); + } + } else { + repositoriesList = Collections.emptyList(); + } + } catch (Exception ex) { + String currentRegistryName = !StringUtils.isBlank(registryId) ? registryId : defaultRegistryId; + logger.error("Could not get repositories info of registry - {}", currentRegistryName); + logger.error("{}", ex.getMessage()); + } + logger.debug("getRepositoriesList finish"); + return repositoriesList; + } + + private Collection getRepositoryImages(String repositoryName, String registryId) { + // aws ecr describe-images [--registry-id ] --repository-name + + logger.debug("getRepositoryImages start"); + // RepositoryName cannot be null/empty + if (StringUtils.isBlank(repositoryName)) { + logger.debug("getRepositoryImages repositoryName is blank/null"); + return Collections.emptyList(); + } + + DescribeImagesRequest request = new DescribeImagesRequest().withRepositoryName(repositoryName); + if (!StringUtils.isBlank(registryId)) { + request = request.withRegistryId(registryId); + logger.debug("getRepositoryImages repositoryName is {}", repositoryName); + } + List imageDetailsList = Collections.emptyList(); + try { + DescribeImagesResult describeImagesResult = amazonClient.describeImages(request); + imageDetailsList = describeImagesResult.getImageDetails(); + } catch (Exception ex) { + String currentRegistryName = !StringUtils.isBlank(registryId) ? registryId : defaultRegistryId; + logger.error("Could not get repository images info of repository {} - on registry - {}", repositoryName, currentRegistryName); + logger.error("{}", ex.getMessage()); + } + logger.debug("getRepositoryImages finish"); + return imageDetailsList; + } + + private List getImagesInformation(String repositoryName, String registryId, String tag, String digest) { + // aws ecr batch-get-image [--registry-id ] --repository-name --image-ids + // --image-ids imageTag=,imageDigest= - can be 1 of them or both + + logger.debug("getImagesInformation start"); + + // RepositoryName cannot be null/empty + if (StringUtils.isBlank(repositoryName)) { + logger.debug("getImagesInformation repositoryName is blank/null"); + return Collections.emptyList(); + } + + // Should be at least Tag or Digest + boolean tagIsEmpty = StringUtils.isBlank(tag); + boolean digestIsEmpty = StringUtils.isBlank(digest); + if (tagIsEmpty && digestIsEmpty) { + logger.debug("getImagesInformation tag && digest are blank/null"); + return Collections.emptyList(); + } + + ImageIdentifier imageIdentifier = new ImageIdentifier(); + if (!tagIsEmpty) { + imageIdentifier = imageIdentifier.withImageTag(tag); + logger.debug("getImagesInformation tag is {}", tag); + } + if (!digestIsEmpty) { + imageIdentifier = imageIdentifier.withImageDigest(digest); + logger.debug("getImagesInformation digest is {}", digest); + } + + List resultImage = null; + BatchGetImageRequest request = new BatchGetImageRequest().withImageIds(imageIdentifier).withRepositoryName(repositoryName); + + if (!StringUtils.isBlank(registryId)) { + logger.debug("getImagesInformation registryId is {}", registryId); + request = request.withRegistryId(registryId); + } + + try { + // Here we got a response that includes 2 lists: + // images list & failures list + BatchGetImageResult response = amazonClient.batchGetImage(request); + if (response != null) { + resultImage = response.getImages(); + List imageFailures = response.getFailures(); + if (imageFailures != null && !imageFailures.isEmpty()) { + logger.info("Errors received when trying to get images:"); + for (ImageFailure imageFailure : imageFailures) { + logger.info("{}", imageFailure); + } + } + } + }catch (Exception ex) { + logger.error("Could not get detailed information for repositoryName - {}", repositoryName); + logger.error("{}", ex.getMessage()); + } + + logger.debug("getImagesInformation finish"); + return resultImage; + } + + private DockerImage getRepositoryImageAsDockerImage(Image image) { + DockerImage resultImage = null; + if (image != null) { + String repositoryName = image.getRepositoryName(); + String manifestInfo = image.getImageManifest(); + // Docker's sha256 different form Amazon's sha256 (but it can be found in Amazon's manifest) + String imageHash = getSHA256FromManifest(manifestInfo); + try { + String tag = image.getImageId().getImageTag(); + resultImage = new DockerImage(repositoryName, tag, imageHash); + } catch (Exception ex) { + logger.error("Could not get image tag of {} ", repositoryName); + logger.error("{}", ex.getMessage()); + } + } + return resultImage; + } + + @Override + protected Set getRemoteRegistryImagesList() { + + logger.debug("getRemoteRegistryImagesList start"); + + List registryIdsList = config.getAmazonRegistryIds(); + // The registry id values should be explicitly defined, so if the user does not define it or use a .*.* value + // then we convert it to empty value - which will be considered by Amazon as the default registry (of the user + // that performed the login) + //Constants. + if (registryIdsList == null || registryIdsList.isEmpty() || registryIdsList.contains(Constants.GLOB_PATTERN)) { + registryIdsList = new LinkedList<>(); + registryIdsList.add(Constants.EMPTY_STRING); + logger.debug("getRemoteRegistryImagesList registryIdsList is default (includes only empty string)"); + } + + //List result = new LinkedList<>(); + Set result = new HashSet<>(); + // Get all repositories of required registry ids + for(String registryId : registryIdsList) { + // Use 'null' instead of 'config.getImageNames()' - because getImageNames() can include repository names + // that do not appear in the current registry id - this will cause an exception from Amazon response and + // we will not get the other available repository names in the response. + // But when using repository names = null -> Amazon will treat it as a request to bring all repositories + logger.debug("getRemoteRegistryImagesList registryId is {}", registryId); + Collection repositoriesList = getRepositoriesList(registryId,null); + if (repositoriesList != null) { + // for each repository (repository = collection of same image with different tags/digests) + for (Repository repository : repositoriesList) { + // repositoryName cannot be null + String repositoryName = repository.getRepositoryName(); + logger.debug("getRemoteRegistryImagesList registryId - {} , repository - {}", registryId, repositoryName); + // Get information about all images in the repository + Collection imageDetailsList = getRepositoryImages(repositoryName, registryId); + // The information is 'ImageDetail' contains the sha256 as Amazon stores it + // But we need the sha256 as Docker stores it + if (imageDetailsList != null) { + // So for each image (repository,registry,digest -> unique key) + for (ImageDetail imageDetail : imageDetailsList) { + String digest = imageDetail.getImageDigest(); + String registry = imageDetail.getRegistryId(); + List tags = imageDetail.getImageTags(); + Date imagePushedAt = imageDetail.getImagePushedAt(); + logger.debug("getRemoteRegistryImagesList registryId - {} , repository - {}, tags - {}", + registryId, repositoryName, tags); + // Get the 'Image' information - it includes the sha256 as Docker stores it + List imagesList = getImagesInformation(repositoryName, registry, Constants.EMPTY_STRING, digest); + if (imagesList != null && !imagesList.isEmpty()) { + for (Image image : imagesList) { + // Convert 'Image' to 'DockerImage' by extracting the Docker sha256 from 'Image' + //DockerImage newDockerImage = getRepositoryImageAsDockerImage(image); + //logger.debug("getRemoteRegistryImagesList - new Docker image - {}", newDockerImage); + //result.add(image.getImageManifest()); + // TODO: change this + String manifest = image.getImageManifest(); + String mainTag = image.getImageId().getImageTag(); + String dockerDigest = getSHA256FromManifest(manifest); + + DockerImageAmazon newDockerImage = new DockerImageAmazon(registry, repositoryName, + digest, tags, imagePushedAt, manifest, mainTag, dockerDigest); + //newDockerImage.setImageSha256(dockerDigest); + + result.add(newDockerImage); + imageDigestToTagMap.put(digest, mainTag); + } + } + } + } + } + } + } + logger.debug("Found {} images", result.size()); + logger.debug("getRemoteRegistryImagesList finish"); + return result; + } + + @Override + protected void logoutRemoteDocker() { + // TODO Check if need to logout from amazon account - We dont login to amazon account ? check with george + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureCli.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureCli.java new file mode 100644 index 0000000..4d04a62 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureCli.java @@ -0,0 +1,64 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker.azure; + +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.utils.Cli; + +public class AzureCli extends Cli { + + /* --- Static members --- */ + + private static final String AZ = "az"; + private static final String LOGIN = AZ + Constants.WHITESPACE + "login"; + private static final String LOGOUT = AZ + Constants.WHITESPACE + "logout"; + private static final String CONTAINER_REGISTRY = AZ + Constants.WHITESPACE + "acr"; + private static final String ACCOUNT_LIST = AZ + Constants.WHITESPACE + "account" + Constants.WHITESPACE + "list"; + private static final String LOGIN_CONTAINER_REGISTRY = CONTAINER_REGISTRY + Constants.WHITESPACE + "login"; + private static final String REPOSITORY = CONTAINER_REGISTRY + Constants.WHITESPACE + "repository"; + private static final String REPOSITORY_LIST = REPOSITORY + Constants.WHITESPACE + "list"; + private static final String REPOSITORY_SHOW_MANIFEST = REPOSITORY + Constants.WHITESPACE + "show-manifests"; + + private static final String USER_NAME_PARAM = "-u"; + private static final String USER_PASSWORD_PARAM = "-p"; + private static final String ACR_NAME_PARAM = "-n"; + private static final String USER_NAME_FULL_PARAM = "--username"; + private static final String VERSION_PARAM = "--version"; + private static final String REPOSITORY_NAME_PARAM = "--repository"; + private static final String REPOSITORY_MANIFEST_ORDER_DESC = "--orderby time_desc"; + + /* --- Public methods --- */ + + public String getBasicCommand() { + return getCommandPrefix() + AZ + Constants.WHITESPACE + VERSION_PARAM; + } + + public String getLoggedInAccountList() { return getCommandPrefix() + ACCOUNT_LIST; } + + public String getLoginCommand(String userName, String password) { + return getCommandPrefix() + LOGIN + Constants.WHITESPACE + USER_NAME_PARAM + Constants.WHITESPACE + userName + Constants.WHITESPACE + + USER_PASSWORD_PARAM + Constants.WHITESPACE + password; + } + + public String getLogoutCommand(String userName) { + return getCommandPrefix() + LOGOUT + Constants.WHITESPACE + USER_NAME_FULL_PARAM + Constants.WHITESPACE + userName; + } + + public String getLoginContainerRegistryCommand(String acrName) { + return getCommandPrefix() + LOGIN_CONTAINER_REGISTRY + Constants.WHITESPACE + ACR_NAME_PARAM + Constants.WHITESPACE + acrName; + } + + public String getRepositoryListCommand(String acrName) { + return getCommandPrefix() + REPOSITORY_LIST + Constants.WHITESPACE + ACR_NAME_PARAM + Constants.WHITESPACE + acrName; + } + + public String getRepositoryShowManifest(String acrName, String repositoryName) { + return getCommandPrefix() + REPOSITORY_SHOW_MANIFEST + Constants.WHITESPACE + ACR_NAME_PARAM + Constants.WHITESPACE + acrName + + Constants.WHITESPACE + REPOSITORY_NAME_PARAM + Constants.WHITESPACE + repositoryName + + Constants.WHITESPACE + REPOSITORY_MANIFEST_ORDER_DESC; + } + + private String getCommandPrefix() { + return DependencyCollector.isWindows() ? Constants.CMD + Constants.WHITESPACE + + DependencyCollector.C_CHAR_WINDOWS + Constants.WHITESPACE : Constants.EMPTY_STRING; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureDockerImage.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureDockerImage.java new file mode 100644 index 0000000..6803850 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureDockerImage.java @@ -0,0 +1,33 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker.azure; + +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.docker.remotedocker.AbstractRemoteDockerImage; + +public class AzureDockerImage extends AbstractRemoteDockerImage { + + /* --- Private fields --- */ + + private String registry; + + /* --- Constructors --- */ + + AzureDockerImage() { + } + + /* --- Getters / Setters --- */ + + public String getRegistry() { + return registry; + } + + public void setRegistry(String registry) { + this.registry = registry; + } + + /* --- Public methods --- */ + + public String getUniqueIdentifier() { + // TODO should be only this.getRegistry() + Constants.FORWARD_SLASH + this.getRepositoryName() , tag should be added outside + return this.getRegistry() + Constants.FORWARD_SLASH + this.getRepositoryName() + Constants.COLON + this.getImageTags().get(0); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureRemoteDocker.java b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureRemoteDocker.java new file mode 100644 index 0000000..15f805b --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/docker/remotedocker/azure/AzureRemoteDocker.java @@ -0,0 +1,249 @@ +package org.whitesource.agent.dependency.resolver.docker.remotedocker.azure; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.io.IOUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.docker.remotedocker.AbstractRemoteDocker; +import org.whitesource.agent.dependency.resolver.docker.remotedocker.AbstractRemoteDockerImage; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.Pair; +import org.whitesource.fs.configuration.RemoteDockerConfiguration; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class AzureRemoteDocker extends AbstractRemoteDocker { + + /* --- Static members --- */ + + private static final Logger logger = LoggerFactory.getLogger(AzureRemoteDocker.class); + private static final String DIGEST = "digest"; + private static final String TAGS = "tags"; + private static final String AZURE_SERVER_NAME = ".azurecr.io"; + + /* --- Private members --- */ + + private AzureCli azureCli; + private boolean loggedInToAzure = false; + private List loggedInRegistries = new ArrayList<>(); + + /* --- Constructors --- */ + + public AzureRemoteDocker(RemoteDockerConfiguration config) { + super(config); + azureCli = new AzureCli(); + } + + /* --- Overridden protected methods --- */ + + /** + * Log in to container registry in Azure cloud. + * If login succeeded, docker obtains authentication to pull images from this registry. + * @return + */ + @Override + protected boolean loginToRemoteRegistry() { + Pair result; + + try { + // Check if user already logged in to azure + isUserLoggedIn(); + + // If user isn't logged in, then login to azure via az cli. + if(!loggedInToAzure) { + Process process = Runtime.getRuntime().exec(azureCli.getLoginCommand(config.getAzureUserName(), config.getAzureUserPassword())); + int resultValue = process.waitFor(); + if (resultValue == 0) { + logger.info("Log in to Azure account {} - Succeeded", config.getAzureUserName()); + } else { + logger.info("Log in to Azure account {} - Failed", config.getAzureUserName()); + String errorMessage = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8.name()); + logger.error("Failed to log in to Azure account {} - {}", config.getAzureUserName(), errorMessage); + return false; + } + } + + // Log in to registry via azure cli, Run azure command: "az acr login -n " + // Once azure login command to registry succeeded - docker obtains authentication to pull images from registry. + for (String registryName : config.getAzureRegistryNames()) { + if (registryName != null && !registryName.trim().equals(Constants.EMPTY_STRING)) { + logger.info("Log in to Azure container registry {}", registryName); + result = executeCommand(azureCli.getLoginContainerRegistryCommand(registryName)); + if (result.getKey() == 0) { + logger.info("Login to registry : {} - Succeeded", registryName); + loggedInRegistries.add(registryName); + } else { + logger.info("Login to registry {} - Failed", registryName); + logger.error("Failed to login registry {} - {}", registryName, IOUtils.toString(result.getValue(), StandardCharsets.UTF_8.name())); + } + } + } + return true; + + } catch (InterruptedException interruptedException) { + logger.debug("Failed to login to Azure account, Exception: {}", interruptedException.getMessage()); + } catch (IOException ioException) { // this exception occurred when parsing inputStream to String + logger.debug("Failed to parse command error result, Exception: {}", ioException.getMessage()); + } + + return false; + } + + @Override + protected Set getRemoteRegistryImagesList() { + Set images = new HashSet<>(); + if (!loggedInRegistries.isEmpty()) { + logger.info("Get list of images for registries : [{}]", String.join(", ", loggedInRegistries)); + Gson gson = new Gson(); + String repositoryCommand; + String[] repositoryNames; + Pair result; + + // registryNames list taken from Config file + for (String registryName : loggedInRegistries) { + repositoryNames = null; + /* Get list of images names in registry + Run azure command: "az acr repository list --name + */ + repositoryCommand = azureCli.getRepositoryListCommand(registryName); + result = executeCommand(repositoryCommand); + try { + if (result.getKey() == 0) { + // Parse json array to string array that contain repositories/images names. + String resultValue = IOUtils.toString(result.getValue(), StandardCharsets.UTF_8.name()); + logger.debug("Azure images names for registry \"{}\" : {}", registryName, resultValue); + + Type type = new TypeToken() {}.getType(); + repositoryNames = gson.fromJson(resultValue, type); + } else { + logger.warn("Failed to get repositories list for registry \"{}\"", registryName); + } + + if (repositoryNames != null) { + for (String repositoryName : repositoryNames) { + AzureDockerImage azureImage = new AzureDockerImage(); + azureImage.setRepositoryName(repositoryName); + azureImage.setRegistry(registryName + AZURE_SERVER_NAME); + + // Get repository manifest, digest and tags via azure cli command "az acr repository show-manifest" + /* [ + { + "digest": "sha256:915f390a8912e16d4beb8689720a17348f3f6d1a7b659697df850ab625ea29d5", + "tags": [ + "v1" + ], + "timestamp": "2018-11-20T14:55:48.3185739Z" + } + ] + */ + repositoryCommand = azureCli.getRepositoryShowManifest(registryName, repositoryName); + result = executeCommand(repositoryCommand); + + if (result.getKey() == 0) { + String resultValue = IOUtils.toString(result.getValue(), StandardCharsets.UTF_8.name()); + logger.debug("Manifest for repository \"{}\": {}", repositoryName, resultValue); + JSONArray jsonArray = new JSONArray(resultValue); + JSONObject jsonObject = jsonArray.getJSONObject(0); + // TODO in case there's more than one digest?! + String digest = jsonObject.getString(DIGEST); + azureImage.setImageDigest(digest); + azureImage.setImageSha256(getSHA256FromManifest(digest)); + //TODO tags not ok + setImageTagsList(azureImage, jsonObject); + + images.add(azureImage); + } else { + logger.warn("Failed to get details of repository {}", repositoryName); + } + } + } + } catch (IOException e) { + logger.warn("Failed to parse command {} result. Exception: {}", repositoryCommand, e.getMessage()); + logger.debug("Failed to parse command {} result. Exception: {}", repositoryCommand, e.getStackTrace()); + } + } + } + + return images; + } + + @Override + protected boolean isRegistryCliInstalled() { + boolean installed = isCommandSuccessful(azureCli.getBasicCommand()); + if (!installed) { + logger.error("Azure CLI is not installed"); + } + return installed; + } + + @Override + protected String getImageFullURL(AbstractRemoteDockerImage image) { + String result = Constants.EMPTY_STRING; + if (image != null) { + // get Unique identifier for azure image "AzureDockerImage" + result = image.getUniqueIdentifier(); + } + return result; + } + + @Override + protected void logoutRemoteDocker() { + // If user isn't logged in, then logout from azure via az cli. + if(!loggedInToAzure) { + logger.debug("Logging out from azure account.."); + boolean loggedOut = isCommandSuccessful(azureCli.getLogoutCommand(config.getAzureUserName())); + if (!loggedOut) { + logger.error("Failed to logout from azure account"); + } + } + } + + private void setImageTagsList(AzureDockerImage azureImage, JSONObject jsonObject) { + List tags = new ArrayList<>(); + JSONArray tagsJsonArray = jsonObject.getJSONArray(TAGS); + for (int i = 0; i < tagsJsonArray.length(); i++) { + tags.add(tagsJsonArray.getString(i)); + } + azureImage.setImageTags(tags); + } + + /** + * Get Azure logged in users account list. + * Check if user logged in. If true then set 'loggedInToAzure' to true and UA will not Login/Logout account. + * + */ + private void isUserLoggedIn() { + String accountListCmd = azureCli.getLoggedInAccountList(); + Pair result = executeCommand(accountListCmd); + if (result.getKey() == 0) { + try { + String accountList = IOUtils.toString(result.getValue(), StandardCharsets.UTF_8.name()); + + JSONArray jsonArray = new JSONArray(accountList); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String loggedInUserName = jsonObject.getJSONObject("user").getString("name"); + if (loggedInUserName.equalsIgnoreCase(config.getAzureUserName())) { + loggedInToAzure = true; + break; + } + } + } catch (IOException e) { + logger.warn("Failed to parse command {} result. Exception: {}", accountListCmd, e.getMessage()); + logger.debug("Failed to parse command {} result. Exception: {}", accountListCmd, e.getStackTrace()); + } + } else { + logger.warn("Failed to get Azure logged in account list, command: {}.", accountListCmd); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetDependencyResolver.java new file mode 100644 index 0000000..4cbb16c --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetDependencyResolver.java @@ -0,0 +1,46 @@ +package org.whitesource.agent.dependency.resolver.dotNet; + +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.dependency.resolver.nuget.NugetDependencyResolver; +import org.whitesource.agent.dependency.resolver.nuget.packagesConfig.NugetConfigFileType; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author raz.nitzan + */ +public class DotNetDependencyResolver extends NugetDependencyResolver { + + /* --- Members --- */ + + private final DotNetRestoreCollector resolveCollector; + private boolean nugetRestoreDependencies; + + /* --- Constructor --- */ + + public DotNetDependencyResolver(String whitesourceConfiguration, NugetConfigFileType nugetConfigFileType, boolean nugetRestoreDependencies, boolean ignoreSourceFiles) { + super(whitesourceConfiguration, nugetConfigFileType, nugetRestoreDependencies, ignoreSourceFiles); + this.nugetRestoreDependencies = nugetRestoreDependencies; + this.resolveCollector = new DotNetRestoreCollector(); + } + + /* --- Overridden methods --- */ + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set csprojFiles) { + if (this.nugetRestoreDependencies) { + this.resolveCollector.executeRestore(projectFolder, csprojFiles); + Collection projects = this.resolveCollector.collectDependencies(projectFolder); + Collection dependencies = projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()); + dependencies.addAll(parseNugetPackageFiles(csprojFiles, true)); + return new ResolutionResult(dependencies, new LinkedList<>(), getDependencyType(), topLevelFolder); + } else { + return getResolutionResultFromParsing(topLevelFolder, csprojFiles, false); + } + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetRestoreCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetRestoreCollector.java new file mode 100644 index 0000000..82e94a3 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/DotNetRestoreCollector.java @@ -0,0 +1,28 @@ +package org.whitesource.agent.dependency.resolver.dotNet; + +import org.whitesource.agent.TempFolders; + +import java.nio.file.Paths; + +/** + * @author raz.nitzan + */ +public class DotNetRestoreCollector extends RestoreCollector { + + /* --- Statics Members --- */ + + private static final String DOTNET_RESTORE_TMP_DIRECTORY = Paths.get(System.getProperty("java.io.tmpdir"), TempFolders.UNIQUE_DOTNET_TEMP_FOLDER).toString(); + private static final String DOTNET_COMMAND = "dotnet"; + private static final String PACKAGES = "--packages"; + + /* --- Constructors --- */ + + public DotNetRestoreCollector() { + super(DOTNET_RESTORE_TMP_DIRECTORY, DOTNET_COMMAND); + } + + @Override + protected String[] getInstallParams(String pathToDownloadPackages, String csprojFile) { + return new String[]{DOTNET_COMMAND, RESTORE, csprojFile, PACKAGES, pathToDownloadPackages}; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/RestoreCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/RestoreCollector.java new file mode 100644 index 0000000..b287984 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/dotNet/RestoreCollector.java @@ -0,0 +1,136 @@ +package org.whitesource.agent.dependency.resolver.dotNet; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.dependency.resolver.npm.NpmLsJsonDependencyCollector; +import org.whitesource.agent.hash.ChecksumUtils; +import org.whitesource.agent.utils.CommandLineProcess; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +/** + * @author raz.nitzan + */ +public abstract class RestoreCollector extends DependencyCollector { + + /* --- Statics Members --- */ + + private static final Logger logger = LoggerFactory.getLogger(NpmLsJsonDependencyCollector.class); + + public static final String NUPKG = ".nupkg"; + public static final String RESTORE = "restore"; + public static final String BACK_SLASH = isWindows() ? Constants.BACK_SLASH : Constants.FORWARD_SLASH; + private static String[] includes = {"**/*" + NUPKG}; + private static String[] excludes = {}; + + /* --- Members --- */ + + private int serialNumber = 0; + private Map tempPathToPackagesFile = new HashMap<>(); + private String tempDirectory; + private String command; + + /* --- Constructors --- */ + + public RestoreCollector(String tempDirectory, String command) { + this.tempDirectory = tempDirectory; + this.command = command; + } + + /* --- Public methods --- */ + + @Override + public Collection collectDependencies(String rootDirectory) { + Collection dependencies = new LinkedList<>(); + Map> folderMapToFiles = new FilesUtils().fillFilesMap(this.tempPathToPackagesFile.keySet(), this.includes, + this.excludes,true, false); + for (File file : folderMapToFiles.keySet()) { + for (String shortPath : folderMapToFiles.get(file)) { + String nugetFilePath = file.getAbsolutePath() + BACK_SLASH + shortPath; + DependencyInfo dependency = getDependency(nugetFilePath, this.tempPathToPackagesFile.get(file.getPath())); + dependencies.add(dependency); + } + } + deleteDirectories(); + logger.debug("Finish deleting directories of {} {}", this.command, RESTORE); + return getSingleProjectList(dependencies); + } + + public void executeRestore(String folder, Set files) { + for (String file : files) { + String pathToDownloadPackages = tempDirectory + BACK_SLASH + getNameOfFolderPackages(file) + this.serialNumber; + this.serialNumber++; + String[] command = getInstallParams(pathToDownloadPackages, file); + String commandString = String.join(Constants.WHITESPACE, command); + logger.debug("Running command : '{}'", commandString); + CommandLineProcess restoreCommandLine = new CommandLineProcess(folder, command); + try { + restoreCommandLine.executeProcess(); + } catch (IOException e) { + logger.warn("Could not run '{}' in folder: {}", commandString, folder); + } + if (!restoreCommandLine.isErrorInProcess()) { + logger.debug("Finish to run '{}'", commandString); + this.tempPathToPackagesFile.put(pathToDownloadPackages, file); + } else { + logger.warn("Could not run '{}' in folder: {}", commandString, folder); + } + } + } + + public String getCommand() { + return this.command; + } + + /* --- abstract methods --- */ + + protected abstract String[] getInstallParams(String pathToDownloadPackages, String csprojFile); + + /* --- Private methods --- */ + + private String getNameOfFolderPackages(String filePath) { + File file = new File(filePath); + String nameWithExtension = file.getName(); + int indexLastDot = nameWithExtension.indexOf(Constants.DOT); + if (indexLastDot > -1) { + return nameWithExtension.substring(0, indexLastDot); + } else { + return nameWithExtension; + } + } + + private void deleteDirectories() { + File mainDirectory = new File(this.tempDirectory); + FilesUtils.deleteDirectory(mainDirectory); + } + + private String getSha1(String filePath) { + try { + return ChecksumUtils.calculateSHA1(new File(filePath)); + } catch (IOException e) { + logger.info("Failed getting " + filePath + ". File will not be send to WhiteSource server."); + return Constants.EMPTY_STRING; + } + } + + private DependencyInfo getDependency(String nugetFilePath, String systemPath) { + DependencyInfo dependency = new DependencyInfo(); + // TODO to fix the issue with the dependency type + // dependency.setDependencyType(DependencyType.NUGET); + dependency.setArtifactId(nugetFilePath.substring(nugetFilePath.lastIndexOf(BACK_SLASH) + 1)); + if (StringUtils.isNotEmpty(systemPath)) { + dependency.setSystemPath(systemPath); + } + String sha1 = getSha1(nugetFilePath); + dependency.setSha1(sha1); + return dependency; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyManager.java b/src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyManager.java new file mode 100644 index 0000000..4bb1c8a --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyManager.java @@ -0,0 +1,30 @@ +package org.whitesource.agent.dependency.resolver.go; + +public enum GoDependencyManager { + DEP("dep"), + GO_DEP("godep"), + VNDR("vndr"), + GO_GRADLE("gogradle"), + GO_VENDOR("govendor"), + GOPM("gopm"), + GLIDE("glide"); + + + private final String type; + + GoDependencyManager(String type){ + this.type = type; + } + + public String getType(){ + return this.type; + } + + public static GoDependencyManager getFromType(String type){ + for (GoDependencyManager goDependencyManager : GoDependencyManager.values()){ + if (goDependencyManager.getType().equals(type.trim())) + return goDependencyManager; + } + return null; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyResolver.java new file mode 100644 index 0000000..f63f376 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/go/GoDependencyResolver.java @@ -0,0 +1,1034 @@ +package org.whitesource.agent.dependency.resolver.go; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.dependency.resolver.gradle.GradleCli; +import org.whitesource.agent.dependency.resolver.gradle.GradleMvnCommand; +import org.whitesource.agent.hash.HashCalculator; +import org.whitesource.agent.utils.Cli; +import org.whitesource.agent.utils.CommandLineProcess; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.*; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.whitesource.agent.Constants.BUILD_GRADLE; +import static org.whitesource.agent.Constants.EMPTY_STRING; + +public class GoDependencyResolver extends AbstractDependencyResolver { + + public static final String GOPM_GEN_CMD = "gen"; + private static final String GODEPS = "Godeps"; + private static final String VENDOR = "vendor"; + private final Logger logger = LoggerFactory.getLogger(GoDependencyResolver.class); + + private static final String PROJECTS = "[[projects]]"; + private static final String DEPS = "Deps"; + private static final String PACKAGE = "package"; + private static final String REV = "Rev"; + private static final String COMMENT = "Comment"; + private static final String IMPORT_PATH = "ImportPath"; + private static final String PATH = "path"; + private static final String NAME = "name"; + private static final String COMMIT = "commit: "; + private static final String VERSION = "version = "; + private static final String VERSION_GOV = "version"; + private static final String REVISION = "revision = "; + private static final String REVISION_GOV = "revision"; + private static final String CHECKSUM_SHA1 = "checksumSHA1"; + private static final String PACKAGES = "packages = "; + private static final String BRACKET = "]"; + private static final String DOT = "."; + private static final String ASTERIX = "(*)"; + private static final String SLASH = "\\--"; + private static final String GOPKG_LOCK = "Gopkg.lock"; + private static final String GODEPS_JSON = "Godeps.json"; + private static final String GOVENDOR_JSON = "vendor.json"; + private static final String VNDR_CONF = "vendor.conf"; + private static final String GOGRADLE_LOCK = "gogradle.lock"; + private static final String GLIDE_LOCK = "glide.lock"; + private static final String GLIDE_YAML = "glide.yaml"; + private static final String GOPM_FILE = ".gopmfile"; + private static final String GO_EXTENSION = ".go"; + private static final String GO_ENSURE = "ensure"; + private static final String GO_INIT = "init"; + private static final String GO_SAVE = "save"; + private static final String GO_ADD_EXTERNAL = "add +external"; + private static final List GO_SCRIPT_EXTENSION = Arrays.asList(GOPKG_LOCK, GODEPS_JSON, VNDR_CONF, + BUILD_GRADLE, GLIDE_LOCK, GLIDE_YAML, GOVENDOR_JSON, + GOPM_FILE); + private static final String IMPORTS = "imports"; + private static final String NAME_GLIDE = "- name: "; + private static final String VERSION_GLIDE = " version: "; + private static final String SUBPACKAGES_GLIDE = " subpackages"; + private static final String PREFIX_SUBPACKAGES_SECTION = " - "; + private static final String TEST_IMPORTS = "testImports"; + private static final String GO_UPDATE = "update"; + private static final String GOPM_DEPS = "deps"; + private static final String OPENNING_BRACKET = "["; + private static final String EQUAL = "="; + private static final String GOPM_TAG = "tag:"; + private static final String GOPM_COMMIT = "commit:"; + private static final String GOPM_BRANCH = "branch:"; + public static String GO_DEPENDENCIES = "goDependencies"; + public static final String GRADLE_LOCK = "lock"; + public static final String GRADLE_GO_LOCK = "goLock"; + + private Cli cli; + private GoDependencyManager goDependencyManager; + private boolean collectDependenciesAtRuntime; + private boolean ignoreSourceFiles; + private boolean ignoreTestPackages; + private boolean goGradleEnableTaskAlias; + private String gradlePreferredEnvironment; + private HashCalculator hashCalculator = new HashCalculator(); + private boolean addSha1; + private boolean afterCollect = false; + + public GoDependencyResolver(GoDependencyManager goDependencyManager, boolean collectDependenciesAtRuntime, boolean ignoreSourceFiles, boolean ignoreTestPackages, boolean goGradleEnableTaskAlias, String gradlePreferredEnvironment, boolean addSha1){ + super(); + this.cli = new Cli(); + this.goDependencyManager = goDependencyManager; + this.collectDependenciesAtRuntime = collectDependenciesAtRuntime; + this.ignoreSourceFiles = ignoreSourceFiles; + this.ignoreTestPackages = ignoreTestPackages; + this.goGradleEnableTaskAlias = goGradleEnableTaskAlias; + this.gradlePreferredEnvironment = gradlePreferredEnvironment; + this.addSha1 = addSha1; + } + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + List dependencies = collectDependencies(topLevelFolder); + return new ResolutionResult(dependencies, getExcludes(), getDependencyType(), topLevelFolder); + } + + @Override + protected Collection getExcludes() { + Set excludes = new HashSet<>(); + if (afterCollect && ignoreSourceFiles){ + excludes.add(Constants.PATTERN + GO_EXTENSION); + } + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return GO_SCRIPT_EXTENSION; + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.GO; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.GO.name(); + } + + @Override + public String[] getBomPattern() { + // when collectDependenciesAtRuntime=false, the FSA should look for the relevant lock/json file, when its true + // the FSA should look for a *.go file, unless when the dependency-manager is go-gradle, don't return *.go but build.gradle + if (goDependencyManager == null || (collectDependenciesAtRuntime && goDependencyManager != GoDependencyManager.GO_GRADLE)) { + return new String[]{Constants.PATTERN + GO_EXTENSION}; + } + if (goDependencyManager != null) { + switch (goDependencyManager) { + case DEP: + return new String[]{Constants.PATTERN + GOPKG_LOCK}; + case GO_DEP: + return new String[]{Constants.PATTERN + GODEPS_JSON}; + case VNDR: + return new String[]{Constants.PATTERN + VNDR_CONF}; + case GO_GRADLE: + return new String[]{Constants.GLOB_PATTERN_PREFIX + Constants.BUILD_GRADLE}; + case GLIDE: + return new String[]{Constants.PATTERN + GLIDE_LOCK, Constants.PATTERN + GLIDE_YAML}; + case GO_VENDOR: + return new String[]{Constants.PATTERN + GOVENDOR_JSON}; + case GOPM: + return new String[]{Constants.PATTERN + GOPM_FILE}; + } + } + return new String[]{EMPTY_STRING}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(GOPKG_LOCK, GOVENDOR_JSON, VNDR_CONF, Constants.BUILD_GRADLE, GLIDE_LOCK, GLIDE_YAML, GOVENDOR_JSON, GOPM_FILE, GO_EXTENSION); + } + + @Override + protected Collection getLanguageExcludes() { + return null; + } + + private List collectDependencies(String rootDirectory) { + List dependencyInfos = new ArrayList<>(); + String error = null; + long creationTime = new Date().getTime(); // will be used later for removing temp files/folders + if (goDependencyManager != null) { + try { + switch (goDependencyManager) { + case DEP: + collectDepDependencies(rootDirectory, dependencyInfos); + break; + case GO_DEP: + collectGoDepDependencies(rootDirectory, dependencyInfos); + break; + case VNDR: + collectVndrDependencies(rootDirectory, dependencyInfos); + break; + case GO_GRADLE: + collectGoGradleDependencies(rootDirectory, dependencyInfos); + break; + case GLIDE: + collectGlideDependencies(rootDirectory, dependencyInfos); + break; + case GO_VENDOR: + collectGoVendorDependencies(rootDirectory, dependencyInfos); + break; + case GOPM: + collectGoPMDependencies(rootDirectory, dependencyInfos); + break; + default: + error = "The selected dependency manager - " + goDependencyManager.getType() + " - is not supported."; + } + } catch (Exception e) { + error = e.getMessage(); + } + } else { + error = collectDependenciesWithoutDefinedManager(rootDirectory, dependencyInfos); + } + if (error != null){ + logger.error(error); + } + + if (collectDependenciesAtRuntime) { + String errors = FilesUtils.removeTempFiles(rootDirectory, creationTime); + if (!errors.isEmpty()){ + logger.error(errors); + } + } + afterCollect = true; + return dependencyInfos; + } + + // when no dependency manager is defined - trying to run one manager after the other, till one succeeds. if not - returning an error + private String collectDependenciesWithoutDefinedManager(String rootDirectory, List dependencyInfos){ + String error = null; + try { + collectDepDependencies(rootDirectory, dependencyInfos); + goDependencyManager = GoDependencyManager.DEP; + } catch (Exception e){ + try { + collectGoDepDependencies(rootDirectory, dependencyInfos); + goDependencyManager = GoDependencyManager.GO_DEP; + } catch (Exception e1){ + try { + collectVndrDependencies(rootDirectory, dependencyInfos); + goDependencyManager = GoDependencyManager.VNDR; + } catch (Exception e2) { + try { + collectGoGradleDependencies(rootDirectory, dependencyInfos); + goDependencyManager = GoDependencyManager.GO_GRADLE; + } catch (Exception e3) { + try { + collectGlideDependencies(rootDirectory, dependencyInfos); + goDependencyManager = GoDependencyManager.GLIDE; + } catch (Exception e4) { + try { + collectGoVendorDependencies(rootDirectory, dependencyInfos); + goDependencyManager = GoDependencyManager.GO_VENDOR; + } catch (Exception e5) { + try { + collectGoPMDependencies(rootDirectory, dependencyInfos); + goDependencyManager = GoDependencyManager.GOPM; + } catch (Exception e6) { + error = "Couldn't collect dependencies - no dependency manager is installed"; + } + } + } + } + } + } + } + return error; + } + + private void collectDepDependencies(String rootDirectory, List dependencyInfos) throws Exception { + logger.debug("collecting dependencies using 'dep'"); + File goPkgLock = new File(rootDirectory + fileSeparator + GOPKG_LOCK); + String error = EMPTY_STRING; + if (goPkgLock.isFile()){ + if (runCmd(rootDirectory, cli.getCommandParams(GoDependencyManager.DEP.getType(), GO_ENSURE)) == false) { + logger.warn("Can't run 'dep ensure' command, output might be outdated. Run the 'dep ensure' command manually."); + } + dependencyInfos.addAll(parseGopckLock(goPkgLock)); + } else if (collectDependenciesAtRuntime) { + if (runCmd(rootDirectory, cli.getCommandParams(GoDependencyManager.DEP.getType(), GO_INIT))) { + dependencyInfos.addAll(parseGopckLock(goPkgLock)); + } else { + error = "Can't run 'dep init' command. Make sure dep is installed and run the 'dep init' command manually."; + } + } else { + error = "Can't find " + GOPKG_LOCK + " file. Run the 'dep init' command."; + } + if (!error.isEmpty()) { + throw new Exception(error); + } + } + + private List parseGopckLock(File goPckLock){ + logger.debug("parsing {}", goPckLock.getPath()); + List dependencyInfos = new ArrayList<>(); + FileReader fileReader = null; + try { + fileReader = new FileReader(goPckLock); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String currLine; + boolean insideProject = false; + boolean resolveRepositoryPackages = false; + boolean useParent = false; + DependencyInfo dependencyInfo = null; + ArrayList repositoryPackages = null; + while ((currLine = bufferedReader.readLine()) != null){ + if (insideProject) { + if (currLine.isEmpty()){ + insideProject = false; + if (dependencyInfo != null) { + if (repositoryPackages == null || useParent) + dependencyInfos.add(dependencyInfo); + if (repositoryPackages != null){ + for (String name : repositoryPackages){ + DependencyInfo packageDependencyInfo = new DependencyInfo(dependencyInfo.getGroupId(), + dependencyInfo.getArtifactId() + Constants.FORWARD_SLASH + name, + dependencyInfo.getVersion()); + packageDependencyInfo.setCommit(dependencyInfo.getCommit()); + packageDependencyInfo.setDependencyType(DependencyType.GO); + if (useParent) { + dependencyInfo.getChildren().add(packageDependencyInfo); + } else { + dependencyInfos.add(packageDependencyInfo); + } + } + repositoryPackages = null; + } + } + } else { + if (resolveRepositoryPackages){ + if (currLine.contains(BRACKET)){ + resolveRepositoryPackages = false; + } else { + String name = getValue(currLine); + if (name.equals(DOT)){ + useParent = true; + } else { + repositoryPackages.add(getValue(currLine)); + } + } + } else if (currLine.contains(NAME + Constants.WHITESPACE + Constants.EQUALS_CHAR + Constants.WHITESPACE)){ + String name = getValue(currLine); + dependencyInfo.setGroupId(getGroupId(name)); + dependencyInfo.setArtifactId(name); + } else if (currLine.contains(VERSION)){ + dependencyInfo.setVersion(getValue(currLine)); + } else if (currLine.contains(REVISION)){ + dependencyInfo.setCommit(getValue(currLine)); + } else if (currLine.contains(PACKAGES) && !currLine.contains(BRACKET)){ + resolveRepositoryPackages = true; + repositoryPackages = new ArrayList<>(); + } else if (currLine.contains(PACKAGES) && !currLine.contains(DOT)){ + // taking care of lines like 'packages = ["spew"]' (and ignoring lines like 'packages = ["."]') + String name = getValue(currLine); + repositoryPackages = new ArrayList<>(Arrays.asList(name)); + } + } + } else if (currLine.equals(PROJECTS)){ + dependencyInfo = new DependencyInfo(); + insideProject = true; + useParent = false; + } + } + } catch (FileNotFoundException e) { + logger.error("Can't find " + goPckLock.getPath()); + } catch (IOException e) { + logger.error("Can't read " + goPckLock.getPath()); + } finally { + if (fileReader != null){ + try { + fileReader.close(); + } catch (IOException e) { + logger.error("can't close {}: {}", goPckLock.getPath(), e.getMessage()); + } + } + } + dependencyInfos.stream().forEach(dependencyInfo -> { + //dependencyInfo.setDependencyFile(goPckLock.getPath()); + dependencyInfo.setDependencyType(DependencyType.GO); + setSha1(dependencyInfo); + + }); + return dependencyInfos; + } + + private void collectGoPMDependencies(String rootDirectory, List dependencyInfos) throws Exception { + logger.debug("collecting dependencies using 'GoPM'"); + File goPMFile = new File(rootDirectory + fileSeparator + GOPM_FILE); + String error = EMPTY_STRING; + if (goPMFile.isFile()){ + dependencyInfos.addAll(parseGoPm(goPMFile)); + } else if (collectDependenciesAtRuntime) { + if (runCmd(rootDirectory, cli.getCommandParams(GoDependencyManager.GOPM.getType(), GOPM_GEN_CMD))) { + dependencyInfos.addAll(parseGoPm(goPMFile)); + } else { + error = "Can't run 'gopm gen' command. Make sure gopm is installed and run the 'gopm gen' command manually."; + logger.warn("FileNotFoundException: {}", error); + } + } else { + error = "Can't find " + GOPM_FILE + " file. Run the 'gopm gen' command."; + logger.warn("FileNotFoundException: {}", error); + } + if (!error.isEmpty()) { + throw new Exception(error); + } + } + + private List parseGoPm(File goPmFile){ + logger.debug("parsing {}", goPmFile.getPath()); + List dependencyInfos = new ArrayList<>(); + FileReader fileReader = null; + try { + fileReader = new FileReader(goPmFile); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String currLine; + //insideDeps is a boolean indicates if we are inside [deps] section in .gopmfile + boolean insideDeps = false; + DependencyInfo dependencyInfo = null; + ArrayList repositoryPackages = null; + while ((currLine = bufferedReader.readLine()) != null){ + if (insideDeps) { //if we are inside the needed section + dependencyInfo = new DependencyInfo(); + if (currLine.isEmpty() ) { + continue; + } + //if we are no longer in [deps] section + else if (currLine.contains(OPENNING_BRACKET) && currLine.contains(BRACKET) && !currLine.contains(GOPM_DEPS)){ + insideDeps = false; + } else { + //example: github.com/astaxie/beego = tag:v1.9.2 , it will be splitted to two, 1- github.com/astaxie/beego , 2-tag:v1.9.2 + String[] line = currLine.split(EQUAL); + if (line.length > 0) { //retrieving info from the first part {github.com/astaxie/beego} + //removing whitespaces + line[0] = line[0].trim(); + dependencyInfo.setGroupId(getGroupId(line[0])); + dependencyInfo.setArtifactId(line[0]); + } + if (line.length > 1) {//retrieving info from the second part {tag:v1.9.2} + line[1] = line[1].trim(); + if (line[1].contains(GOPM_TAG)) { //tag:v1.9.2 + dependencyInfo.setVersion(line[1].substring(GOPM_TAG.length())); //extract the value after tag: + } else if (line[1].contains(GOPM_COMMIT)) {//commit:a210eea3bd1c3766d76968108dfcd83c331f549c + dependencyInfo.setCommit(line[1].substring(GOPM_COMMIT.length()));//extract the value after commit: + } else if (line[1].contains(GOPM_BRANCH)) { //branch:master + //toDo add branch + //dependencyInfo.(line[1].substring(GOPM_BRANCH.length())); + logger.warn("Using branch to define dependency is not supported, library {} will not be recognized by WSS", line[0]); + } + } + if (line.length <= 1 || line[1].equals(EMPTY_STRING)) { + logger.warn("Using dependency without tag/commit is not supported, library {}, will not be recognized by WSS", line[0]); + continue; + } + /*dependencyInfo.setDependencyType(DependencyType.GO); + dependencyInfo.setSystemPath(goPmFile.getPath()); + setSha1(dependencyInfo);*/ + dependencyInfos.add(dependencyInfo); + } + } else if (currLine.contains(OPENNING_BRACKET + GOPM_DEPS + BRACKET)){ //if the current line contains [deps] + insideDeps = true; + } + } + } catch (FileNotFoundException e) { + logger.warn("FileNotFoundException: {}", e.getMessage()); + logger.debug("FileNotFoundException: {}", e.getStackTrace()); + } catch (IOException e) { + logger.warn("IOException: {}", e.getMessage()); + logger.debug("IOException: {}", e.getStackTrace()); + } finally { + if (fileReader != null){ + try { + fileReader.close(); + } catch (IOException e) { + logger.warn("IOException: {}", e.getMessage()); + logger.debug("IOException: {}", e.getStackTrace()); + } + } + } + dependencyInfos.stream().forEach(dependencyInfo -> { + //dependencyInfo.setDependencyFile(goPmFile.getPath()); + dependencyInfo.setDependencyType(DependencyType.GO); + setSha1(dependencyInfo); + }); + return dependencyInfos; + } + + private String getValue(String line){ + int firstIndex = line.indexOf(Constants.QUOTATION_MARK); + int lastIndex = line.lastIndexOf(Constants.QUOTATION_MARK); + String value = line.substring(firstIndex + 1, lastIndex); + return value; + } + + private void collectGoDepDependencies(String rootDirectory, List dependencyInfos) throws Exception { + logger.debug("collecting dependencies using 'godep'"); + // apparently when go.collectDependenciesAtRuntime=false, the rootDirectory includes the 'Godeps' folder as well - in such case removing it from the path + File goDepJson = new File(rootDirectory + (!collectDependenciesAtRuntime && rootDirectory.endsWith(GODEPS) ? "" : fileSeparator + GODEPS) + fileSeparator + GODEPS_JSON); + if (goDepJson.isFile() || (collectDependenciesAtRuntime && runCmd(rootDirectory, cli.getCommandParams(GoDependencyManager.GO_DEP.getType(), GO_SAVE)))){ + dependencyInfos.addAll(parseGoDeps(goDepJson)); + } else { + throw new Exception("Can't find " + GODEPS_JSON + " file. Please make sure 'godep' is installed and run 'godep save' command"); + } + } + + private List parseGoDeps(File goDeps) throws IOException { + List dependencyInfos = new ArrayList<>(); + HashMap dependencyInfoHashMap = new HashMap<>(); + JsonParser parser = new JsonParser(); + FileReader fileReader = null; + try { + fileReader = new FileReader(goDeps.getPath()); + JsonElement element = parser.parse(fileReader); + if (element.isJsonObject()){ + JsonArray deps = element.getAsJsonObject().getAsJsonArray(DEPS); + DependencyInfo dependencyInfo; + for (int i = 0; i < deps.size(); i++){ + dependencyInfo = new DependencyInfo(); + JsonObject dep = deps.get(i).getAsJsonObject(); + String importPath = dep.get(IMPORT_PATH).getAsString(); + dependencyInfo.setGroupId(getGroupId(importPath)); + dependencyInfo.setArtifactId(importPath); + dependencyInfo.setCommit(dep.get(REV).getAsString()); + dependencyInfo.setDependencyType(DependencyType.GO); + //dependencyInfo.setDependencyFile(goDeps.getPath()); + setSha1(dependencyInfo); + JsonElement commentElement = dep.get(COMMENT); + if (commentElement != null){ + String comment = commentElement.getAsString(); + if (comment.indexOf(Constants.DASH) > -1) { + comment = comment.substring(0, comment.indexOf(Constants.DASH)); + } + dependencyInfo.setVersion(comment); + } + setInHierarchyTree(dependencyInfos, dependencyInfoHashMap, dependencyInfo, importPath); + //dependencyInfos.add(dependencyInfo); + } + } + } catch (FileNotFoundException e){ + logger.warn("FileNotFoundException: {}", e.getMessage()); + logger.debug("FileNotFoundException: {}", e.getStackTrace()); + } finally { + if (fileReader != null){ + fileReader.close(); + } + } + return dependencyInfos; + } + + private String getGroupId(String name){ + String groupId = EMPTY_STRING; + if (name.contains(Constants.FORWARD_SLASH)) { + String[] split = name.split( Constants.FORWARD_SLASH); + groupId = split[1]; + } + return groupId; + } + + private void collectGoVendorDependencies(String rootDirectory, List dependencyInfos) throws Exception { + logger.debug("collecting dependencies using 'govendor'"); + // apparently when go.collectDependenciesAtRuntime=false, the rootDirectory includes the 'vendor' folder as well - in such case removing it from the path + File goVendorJson = new File(rootDirectory + (!collectDependenciesAtRuntime && rootDirectory.endsWith(VENDOR) ? "" : fileSeparator + VENDOR ) + fileSeparator + GOVENDOR_JSON); + if (goVendorJson.isFile() || (collectDependenciesAtRuntime && runCmd(rootDirectory, cli.getCommandParams(GoDependencyManager.GO_VENDOR.getType(), GO_INIT)) && + runCmd(rootDirectory, cli.getCommandParams(GoDependencyManager.GO_VENDOR.getType(), GO_ADD_EXTERNAL)))){ + dependencyInfos.addAll(parseGoVendor(goVendorJson)); + } else { + throw new Exception("Can't find " + GOVENDOR_JSON + " file. Please make sure 'govendor' is installed and run 'govendor init' command"); + } + } + + private List parseGoVendor(File goVendor) throws IOException { + List dependencyInfos = new ArrayList<>(); + HashMap dependencyInfoHashMap = new HashMap<>(); + JsonParser parser = new JsonParser(); + FileReader fileReader = null; + //parse GoVendor dependency json file + try { + fileReader = new FileReader(goVendor.getPath()); + JsonElement dependencyElement = parser.parse(fileReader); + if (dependencyElement.isJsonObject()){ + //foreach dependency info get relevant parameters + JsonArray packages = dependencyElement.getAsJsonObject().getAsJsonArray(PACKAGE); + if (packages != null) { + logger.debug("Number of packeges in json: {}", packages.size()); + DependencyInfo dependencyInfo; + for (int i = 0; i < packages.size(); i++) { + logger.debug("Packeges in json #{} : {}", i, packages.get(i).toString()); + dependencyInfo = new DependencyInfo(); + JsonObject pck = packages.get(i).getAsJsonObject(); + String name = pck.get(PATH).getAsString(); + dependencyInfo.setGroupId(getGroupId(name)); + dependencyInfo.setArtifactId(name); + dependencyInfo.setCommit(pck.get(REVISION_GOV).getAsString()); + dependencyInfo.setDependencyType(DependencyType.GO); + //dependencyInfo.setDependencyFile(goVendor.getPath()); + setSha1(dependencyInfo); + if (pck.get(VERSION_GOV) != null) { + dependencyInfo.setVersion(pck.get(VERSION_GOV).getAsString()); + } + setInHierarchyTree(dependencyInfos, dependencyInfoHashMap, dependencyInfo, name); + } + } + } + } catch (FileNotFoundException e){ + logger.warn("FileNotFoundException: {}", e.getMessage()); + logger.debug("FileNotFoundException: {}", e.getStackTrace()); + } finally { + if (fileReader != null){ + fileReader.close(); + } + } + return dependencyInfos; + } + + private void setInHierarchyTree(List dependencyInfos, HashMap dependencyInfoHashMap, DependencyInfo dependencyInfo, String name) { + boolean childDependency = false; + dependencyInfoHashMap.put(name, dependencyInfo); + // checking if the dependency is child of another (if its name is contained inside the name of other dependency) + while (name.contains(Constants.FORWARD_SLASH)){ + name = name.substring(0, name.lastIndexOf(Constants.FORWARD_SLASH)); + if (dependencyInfoHashMap.get(name) != null){ + dependencyInfoHashMap.get(name).getChildren().add(dependencyInfo); + childDependency = true; + break; + } + } + if (!childDependency){ + dependencyInfos.add(dependencyInfo); + } + } + + private void collectVndrDependencies(String rootDirectory, List dependencyInfos) throws Exception { + logger.debug("collecting dependencies using 'vndr'"); + File vndrConf = new File(rootDirectory + fileSeparator + VNDR_CONF); + if (vndrConf.isFile() || (collectDependenciesAtRuntime && runCmd(rootDirectory, + cli.getCommandParams(GoDependencyManager.VNDR.getType(), GO_INIT)))) { + dependencyInfos.addAll(parseVendorConf(vndrConf)); + } else { + throw new Exception("Can't find " + VNDR_CONF + " file. Please make sure 'vndr' is installed and run 'vndr init' command"); + } + } + + private List parseVendorConf(File vendorConf) throws IOException { + List dependencyInfos = new ArrayList<>(); + FileReader fileReader = null; + try { + fileReader = new FileReader(vendorConf); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String currLine; + DependencyInfo dependencyInfo; + while ((currLine = bufferedReader.readLine()) != null){ + dependencyInfo = new DependencyInfo(); + String[] split = currLine.split(Constants.WHITESPACE); + String name = split[0]; + dependencyInfo.setGroupId(getGroupId(name)); + dependencyInfo.setArtifactId(name); + dependencyInfo.setCommit(split[1]); + dependencyInfo.setDependencyType(DependencyType.GO); + //dependencyInfo.setDependencyFile(vendorConf.getPath()); + setSha1(dependencyInfo); + dependencyInfos.add(dependencyInfo); + } + } catch (FileNotFoundException e) { + throw e; + } finally { + fileReader.close(); + } + return dependencyInfos; + } + + private void collectGoGradleDependencies(String rootDirectory, List dependencyInfos) throws Exception { + logger.debug("collecting dependencies using 'GoGradle'"); + GradleCli gradleCli = new GradleCli(this.gradlePreferredEnvironment); + // WSE-1076 - this is actually a go-gradle issue, not gradle; + // one cannot operate twice on the same stream - i.e. check the stream's count followed by forEach. + // therefore putting the stream into a Supplier object, from which the stream can be accessed multiple times using 'get' + Supplier> supplier = ()-> { + try { + return Files.walk(Paths.get(rootDirectory), Integer.MAX_VALUE).filter(file -> file.getFileName().toString().equals(Constants.BUILD_GRADLE)); + } catch (IOException e) { + logger.warn("Error collecting go-gradle dependencies from {}, exception: {}", rootDirectory, e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } + return null; + }; + if (supplier != null && supplier.get().count() > 0) { + supplier.get().forEach(file -> { + GradleMvnCommand command = this.goGradleEnableTaskAlias ? GradleMvnCommand.GO_DEPENDENCIES : GradleMvnCommand.DEPENDENCIES; + List lines = gradleCli.runGradleCmd(file.getParent().toString(), gradleCli.getGradleCommandParams(command), true); + if (lines != null) { + parseGoGradleDependencies(lines, dependencyInfos, rootDirectory); + if (dependencyInfos.size() > 0) { + File goGradleLock = new File(rootDirectory + fileSeparator + GOGRADLE_LOCK); + // in case goGradle-enable-task-alias is true - use goLock instead of lock + if (goGradleLock.isFile() || (collectDependenciesAtRuntime && runCmd(rootDirectory, gradleCli.getGradleCommandParams(this.goGradleEnableTaskAlias ? GradleMvnCommand.GO_LOCK : GradleMvnCommand.LOCK)))) { + HashMap gradleLockFile = parseGoGradleLockFile(goGradleLock); + // for each dependency - matching its full commit id + dependencyInfos.stream().forEach(dependencyInfo -> dependencyInfo.setCommit(gradleLockFile.get(dependencyInfo.getArtifactId()))); + // removing dependencies without commit-id and version + dependencyInfos.stream().forEach(dependencyInfo -> { + if (dependencyInfo.getVersion() == null && dependencyInfo.getCommit() == null) { + logger.debug("{}/{} has no version nor commit-id; removing it from the dependencies' list", dependencyInfo.getArtifactId(), dependencyInfo.getGroupId()); + } else { + setSha1(dependencyInfo); + } + }); + dependencyInfos.removeIf(dependencyInfo -> dependencyInfo.getCommit() == null && dependencyInfo.getVersion() == null); + } else { + logger.warn("Can't find {} and verify dependencies commit-ids; make sure 'collectDependenciesAtRuntime' is set to true or run 'gradlew lock' manually", goGradleLock.getPath()); + } + } else { + logger.warn("no dependencies found after running 'gradlew " + command.getCommand() + " command. \n" + + "If your gradle.properties file includes 'org.gradle.jvmargs=-Dgogradle.alias=true' make sure that 'go.gogradle.enableTaskAlias' in the configuration file is set to 'true'"); + } + } else { + logger.warn("running `gradlew " + command.getCommand() + "` command failed. \n" + + "If your gradle.properties file includes 'org.gradle.jvmargs=-Dgogradle.alias=true' make sure that 'go.gogradle.enableTaskAlias' in the configuration file is set to 'true';\n" + + "otherwise - set it to false"); + } + }); + } else { + throw new Exception("Can't find any 'build.gradle' file. Please make sure Gradle is installed"); + } + } + + private void parseGoGradleDependencies(List lines, List dependencyInfos, String rootDirectory){ + List filteredLines = lines.stream() + .filter(line->(line.contains(SLASH) || line.contains(Constants.PIPE)) && !line.contains(ASTERIX)) + .collect(Collectors.toList()); + DependencyInfo dependencyInfo; + Pattern shortIdInBracketsPattern = Pattern.compile("\\([a-z0-9]{7}\\)"); + Pattern shortIdPattern = Pattern.compile("[a-z0-9]{7}"); + for (String currentLine : filteredLines){ + /* possible lines: + |-- github.com/astaxie/beego:053a075 + |-- golang.org/x/crypto:github.com/astaxie/beego#053a075344c118a5cc41981b29ef612bb53d20ca/vendor/golang.org/x/crypto + | \-- gopkg.in/yaml.v2:github.com/astaxie/beego#053a075344c118a5cc41981b29ef612bb53d20ca/vendor/gopkg.in/yaml.v2 -> v2.2.1(5420a8b) + |-- github.com/eRez-ws/go-stringUtil:v1.0.4(99cfd8b) + + splitting them using : - the first part contains the name, the second part might contain the version and short-commit-id + */ + try { + dependencyInfo = new DependencyInfo(); + String[] dependencyLineSplit = currentLine.split(Constants.COLON); + // extracting the group and artifact id from the first part of the line + String name = dependencyLineSplit[0]; + int lastSpace = name.lastIndexOf(Constants.WHITESPACE); + name = name.substring(lastSpace + 1); + dependencyInfo.setGroupId(getGroupId(name)); + if (dependencyLineSplit.length > 1) { // extracting the version from the second part + String versionPart = dependencyLineSplit[1]; + Matcher matcher = shortIdInBracketsPattern.matcher(versionPart); + if (matcher.find()) { // extracting version (if found) + int index = matcher.start(); + String version; + if (versionPart.contains(Constants.WHITESPACE) && versionPart.lastIndexOf(Constants.WHITESPACE) < index) { + version = versionPart.substring(versionPart.lastIndexOf(Constants.WHITESPACE), index); + } else { + version = versionPart.substring(0, index); + } + dependencyInfo.setVersion(version); + } else { + matcher = shortIdPattern.matcher(versionPart); + if (matcher.find()){ // extracting short commit id (if found) + int index = matcher.start(); + if (index == 0) { + String shortCommit = versionPart.substring(0,7); + dependencyInfo.setCommit(shortCommit); + } + } + } + } + dependencyInfo.setArtifactId(name); + dependencyInfo.setDependencyType(DependencyType.GO); + //dependencyInfo.setDependencyFile(rootDirectory + fileSeparator + Constants.BUILD_GRADLE); + dependencyInfos.add(dependencyInfo); + } catch (Exception e){ + logger.warn("Error parsing line {}, exception: {}", currentLine, e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } + } + } + + /* + parsing such lines - + apiVersion: "0.10" + dependencies: + build: + - name: "golang.org/x/crypto" + host: + name: "github.com/astaxie/beego" + commit: "053a075344c118a5cc41981b29ef612bb53d20ca" + urls: + - "https://github.com/astaxie/beego.git" + - "git@github.com:astaxie/beego.git" + vcs: "git" + vendorPath: "vendor/golang.org/x/crypto" + transitive: false + - urls: + - "https://github.com/google/go-querystring.git" + - "git@github.com:google/go-querystring.git" + vcs: "git" + name: "github.com/google/go-querystring" + commit: "53e6ce116135b80d037921a7fdd5138cf32d7a8a" + transitive: false + + ignore lines not starting with 2 white-spaces or starting with 4 white-spaces & a dash or 6 white-spaces (meaning indentation) + extract only the name and commit + */ + private HashMap parseGoGradleLockFile(File file){ + HashMap dependenciesCommits = new HashMap<>(); + if (file.isFile()){ + FileReader fileReader = null; + try { + fileReader = new FileReader(file); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String currLine; + String name = null; + String commit = null; + String WS = Constants.WHITESPACE; + while ((currLine = bufferedReader.readLine()) != null) { + if (currLine.startsWith(WS + WS) == false || currLine.startsWith(WS + WS + WS + WS + Constants.DASH) || currLine.startsWith(WS + WS + WS + WS + WS + WS)) + continue; + if (currLine.startsWith(WS + WS + Constants.DASH + WS)) { // start of a block + if (name != null && commit != null){ + dependenciesCommits.put(name, commit); // add previous block (if found) + } + name = null; + commit = null; + } + // WSE-823 - goGradle.lock file may contain quotation marks, apostrophes (probably - didn't meet any such example yet) or none + if (currLine.contains(NAME + Constants.COLON + WS)) { + name = currLine.substring(currLine.indexOf(Constants.COLON) + 1).trim(); + name = name.replace(Constants.QUOTATION_MARK, EMPTY_STRING); + name = name.replace(Constants.APOSTROPHE, EMPTY_STRING); + } else if (currLine.contains(COMMIT)) { + commit = currLine.substring(currLine.indexOf(Constants.COLON) + 1).trim(); + commit = commit.replace(Constants.QUOTATION_MARK, EMPTY_STRING); + commit = commit.replace(Constants.APOSTROPHE, EMPTY_STRING); + } + } + if (name != null && commit != null){ // finished last block + dependenciesCommits.put(name, commit); + } + } catch (FileNotFoundException e) { + logger.warn("Error finding {}, exception: {}", file.getPath(), e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } catch (IOException e) { + logger.warn("Error parsing {}, exception: {}", file.getName(), e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } finally { + try { + fileReader.close(); + } catch (IOException e) { + logger.warn("Can't close {}, exception: {}", file.getName(), e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } + } + } + return dependenciesCommits; + } + + private void collectGlideDependencies(String rootDirectory, List dependencyInfos) throws Exception { + logger.debug("collecting dependencies using 'Glide'"); + File glideLock = new File(rootDirectory + fileSeparator + GLIDE_LOCK); + if (glideLock.isFile()) { + dependencyInfos.addAll(parseGlideLock(glideLock)); + } else if (collectDependenciesAtRuntime) { + File glideYaml = new File(rootDirectory + fileSeparator + GLIDE_YAML); + if (glideYaml.isFile()) { + if (runCmd(rootDirectory, cli.getCommandParams(GoDependencyManager.GLIDE.getType(), GO_UPDATE))) { + if (glideLock.isFile()) { + dependencyInfos.addAll(parseGlideLock(glideLock)); + } else { + throw new Exception("Can't find " + GLIDE_LOCK + " file after running 'glide update'. Please make sure 'Glide' is installed and run 'Glide update' command"); + } + } else { + throw new Exception("Can't find " + GLIDE_LOCK + " file. Please make sure 'Glide' is installed and run 'Glide update' command"); + } + } else { + throw new Exception("Can't find " + GLIDE_YAML + " file. Please make sure 'Glide' is installed and run 'Glide init' command"); + } + } else { + throw new Exception("Can't find " + GLIDE_LOCK + " file. Please make sure 'Glide' is installed and run 'Glide update' command"); + } + } + + private Collection parseGlideLock(File glideLock) { + Collection dependencies = new LinkedList<>(); + BufferedReader bufferedReader = null; + try { + bufferedReader = new BufferedReader(new FileReader(glideLock)); + String currLine; + String name = null; + String commit = null; + // this flag indicates if we get to imports line + boolean resolveRepositoryPackages = false; + boolean resolveSubPackages = false; + DependencyInfo currentDependency = null; + while ((currLine = bufferedReader.readLine()) != null) { + /* possible lines: + imports: + - name: github.com/json-iterator/go + version: 1624edc4454b8682399def8740d46db5e4362ba4 + - name: github.com/modern-go/concurrent + version: bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94 + */ + if (!resolveRepositoryPackages && (currLine.startsWith(IMPORTS) || currLine.startsWith(TEST_IMPORTS))) { + if (currLine.startsWith(TEST_IMPORTS) && this.ignoreTestPackages) { + break; + } + resolveRepositoryPackages = true; + } else if (resolveRepositoryPackages) { + if (currLine.startsWith(NAME_GLIDE)) { + resolveSubPackages = false; + name = currLine.substring(NAME_GLIDE.length()); + currLine = bufferedReader.readLine(); + if (currLine != null) { + commit = currLine.substring(VERSION_GLIDE.length()); + currentDependency = createGlideDependency(name, commit, glideLock.getAbsolutePath()); + dependencies.add(currentDependency); + } + } else if (currLine.startsWith(SUBPACKAGES_GLIDE)) { + resolveSubPackages = true; + } else if (resolveSubPackages && currLine.startsWith(PREFIX_SUBPACKAGES_SECTION)) { + String subPackageName = currLine.substring(PREFIX_SUBPACKAGES_SECTION.length()); + DependencyInfo childDependency = createGlideDependency(name + Constants.FORWARD_SLASH + subPackageName, commit, glideLock.getAbsolutePath()); + if (currentDependency == null) { + dependencies.add(childDependency); + } else { + currentDependency.getChildren().add(childDependency); + } + } else if (currLine.startsWith(TEST_IMPORTS)) { + resolveSubPackages = false; + if (this.ignoreTestPackages) { + break; + } + } else { + resolveSubPackages = false; + } + } + } + } catch (IOException e) { + logger.warn("Error parsing {}, exception: {}", glideLock.getName(), e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + } catch (IOException e) { + logger.warn("Can't close {}, exception: {}", glideLock.getName(), e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } + } + return dependencies; + } + + private DependencyInfo createGlideDependency(String name, String commit, String systemPath) { + DependencyInfo dependency = new DependencyInfo(); + dependency.setArtifactId(name); + dependency.setCommit(commit); + dependency.setDependencyType(DependencyType.GO); + //dependency.setDependencyFile(systemPath); + setSha1(dependency); + return dependency; + } + + private void setSha1(DependencyInfo dependencyInfo){ + if (this.addSha1) { + String artifactId = dependencyInfo.getArtifactId(); + String version = dependencyInfo.getVersion(); + String commit = dependencyInfo.getCommit(); + if (StringUtils.isBlank(version) && StringUtils.isBlank(commit)) { + logger.debug("Unable to calcluate SHA1 for {}, it has no version nor commit-id", artifactId); + return; + } + String sha1 = null; + String sha1Source = StringUtils.isNotBlank(version) ? version : commit; + try { + sha1 = this.hashCalculator.calculateSha1ByNameVersionAndType(artifactId, sha1Source, DependencyType.GO); + } catch (IOException e) { + logger.debug("Failed to calculate sha1 of: {}", artifactId); + } + if (sha1 != null) { + dependencyInfo.setSha1(sha1); + } + } + } + + private boolean runCmd(String rootDirectory, String[] params){ + try { + CommandLineProcess commandLineProcess = new CommandLineProcess(rootDirectory, params); + List a = commandLineProcess.executeProcessWithErrorOutput(); + if (!commandLineProcess.isErrorInProcess()) { + return true; + } + } catch (IOException e) { + logger.warn("Error getting dependencies after running {} on {}, {}" , params , rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + return false; + } + + // when running the dependency manager at run time different files (and folders) are created. removing them according to their creatin time + private void removeTempFiles(String rootDirectory, long creationTime){ + FileTime fileCreationTime = FileTime.fromMillis(creationTime); + File directory = new File(rootDirectory); + File[] fList = directory.listFiles(); + if (fList != null) { + for (File file : fList) { + try { + BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class); + if (fileAttributes.creationTime().compareTo(fileCreationTime) > 0){ + FileUtils.forceDelete(file); + } + } catch (IOException e) { + logger.error("can't remove {}: {}", file.getPath(), e.getMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleCli.java b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleCli.java new file mode 100644 index 0000000..8c20afc --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleCli.java @@ -0,0 +1,81 @@ +package org.whitesource.agent.dependency.resolver.gradle; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.utils.Cli; +import org.whitesource.agent.utils.CommandLineProcess; + +import java.io.IOException; +import java.util.List; + +public class GradleCli extends Cli { + private final Logger logger = LoggerFactory.getLogger(org.whitesource.agent.dependency.resolver.gradle.GradleCli.class); + + protected static final String GRADLE_ASSEMBLE = "assemble"; + protected static final String GRADLE_PROJECTS = "projects"; + private final String GRADLE_COMMAND = "gradle"; + private final String GRADLE_COMMAND_W_WINDOWS = "gradlew"; + private final String GRADLE_COMMAND_W_LINUX = "./gradlew"; + + private String topLevelFolderGradlew = null; + + private String preferredEnvironment; + + public GradleCli(String preferredEnvironment) { + super(); + this.preferredEnvironment = preferredEnvironment; + } + + public List runGradleCmd(String rootDirectory, String[] params, boolean firstTime) { + try { + // run gradle dependencies to get dependency tree + CommandLineProcess commandLineProcess = new CommandLineProcess(rootDirectory, params); + List lines = commandLineProcess.executeProcess(); + if (commandLineProcess.isErrorInProcess()) { + // in case gradle is not installed on the local machine, using 'gradlew' command, which uses local gradle wrapper + this.preferredEnvironment = this.preferredEnvironment.equals(Constants.GRADLE_WRAPPER) ? Constants.GRADLE : Constants.GRADLE_WRAPPER; + params = getGradleCommandParams(GradleMvnCommand.DEPENDENCIES); + commandLineProcess = new CommandLineProcess(rootDirectory, params); + lines = commandLineProcess.executeProcess(); + if (!commandLineProcess.isErrorInProcess()) { + return lines; + } + } else { + return lines; + } + } catch (IOException e) { + if (firstTime && StringUtils.isNotBlank(params[0]) && params[0].contains(GRADLE_COMMAND)) { + this.preferredEnvironment = this.preferredEnvironment.equals(Constants.GRADLE_WRAPPER) ? Constants.GRADLE : Constants.GRADLE_WRAPPER; + params = getGradleCommandParams(GradleMvnCommand.DEPENDENCIES); + // calling 'runGradleCmd' recursively only once, for otherwise there will be a stack-over-flow error + return runGradleCmd(rootDirectory, params, false); + } else { + logger.warn("Error getting results after running Gradle command {} on {}, {}", params, rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + } + return null; + } + + public String[] getGradleCommandParams(GradleMvnCommand command) { + String gradleCommand; + // WSE-753 - use the default gradle environment, set from the config file + if (preferredEnvironment.equals(Constants.GRADLE_WRAPPER)) { + if (this.topLevelFolderGradlew != null) { + gradleCommand = this.topLevelFolderGradlew + Constants.FORWARD_SLASH + GRADLE_COMMAND_W_WINDOWS; + } else { + gradleCommand = DependencyCollector.isWindows() ? GRADLE_COMMAND_W_WINDOWS : GRADLE_COMMAND_W_LINUX; + } + } else { + gradleCommand = GRADLE_COMMAND; + } + return super.getCommandParams(gradleCommand, command.getCommand()); + } + + public void setTopLevelFolderGradlew(String topLevelFolderGradlew) { + this.topLevelFolderGradlew = topLevelFolderGradlew; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleDependencyResolver.java new file mode 100644 index 0000000..54ebbe9 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleDependencyResolver.java @@ -0,0 +1,405 @@ +package org.whitesource.agent.dependency.resolver.gradle; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.fs.Main; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GradleDependencyResolver extends AbstractDependencyResolver { + + /* --- Static members --- */ + + private static final List GRADLE_SCRIPT_EXTENSION = Arrays.asList(".gradle", ".groovy", ".java", ".jar", ".war", ".ear", ".car", ".class"); + + private static final String JAR_EXTENSION = Constants.DOT + Constants.JAR; + private static final String PROJECT = "--- Project"; + public static final String COPY_DEPENDENCIES_TASK_TXT = "copyDependenciesTask.txt"; + private static final String DEPENDENCIES = "dependencies"; + private static final String CURLY_BRACKETS_OPEN = "{"; + private static final String CURLY_BRACKTES_CLOSE = "}"; + private static final String TASK_COPY_DEPENDENCIES_HEADER = "task copyDependencies(type: Copy) {"; + private static final String TASK_COPY_DEPENDENCIES_FOOTER = " into \"lib\""; + + /* --- Private Members --- */ + + private String[] ignoredScopes; + private GradleLinesParser gradleLinesParser; + private GradleCli gradleCli; + private ArrayList topLevelFoldersNames; + private boolean ignoreSourceCode; + private boolean gradleAggregateModules; + private boolean gradleRunPreStep; + private HashMap> dependencyTrees; + + + private final Logger logger = LoggerFactory.getLogger(GradleDependencyResolver.class); + + /* --- Constructors --- */ + + public GradleDependencyResolver(boolean runAssembleCommand, boolean ignoreSourceCode, boolean gradleAggregateModules, String gradlePreferredEnvironment, String[] gradleIgnoredScopes, + String gradleLocalRepositoryPath, boolean gradleRunPreStep) { + super(); + gradleCli = new GradleCli(gradlePreferredEnvironment); + gradleLinesParser = new GradleLinesParser(runAssembleCommand, gradleCli, gradleLocalRepositoryPath); + this.ignoredScopes = gradleIgnoredScopes; + topLevelFoldersNames = new ArrayList<>(); + this.ignoreSourceCode = ignoreSourceCode; + this.gradleAggregateModules = gradleAggregateModules; + this.gradleRunPreStep = gradleRunPreStep; + this.dependencyTrees = new HashMap<>(); + } + + /* --- Overridden methods --- */ + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + logger.debug("gradleAggregateModules={}",gradleAggregateModules); + // In order to use the gradle wrapper, we define the top folder that contains the wrapper + this.gradleCli.setTopLevelFolderGradlew(topLevelFolder); + // each bom-file ( = build.gradle) represents a module - identify its folder and scan it using 'gradle dependencies' + Map projectInfoPathMap = new HashMap<>(); + Collection excludes = new HashSet<>(); + + // // Get the list of projects as paths + // List projectsList = null; + // if (bomFiles.size() > 1) { + // projectsList = collectProjects(topLevelFolder); + // } + // if (projectsList == null) { + // logger.warn("Command \"gradle projects\" did not return a list of projects"); + // } + if (gradleRunPreStep) { + downloadMissingDependencies(projectFolder); + } + + for (String bomFile : bomFiles) { + String bomFileFolder = new File(bomFile).getParent(); + File bomFolder = new File(new File(bomFile).getParent()); + String moduleName = bomFolder.getName(); + // String moduleRelativeName = Constants.EMPTY_STRING; + // try { + // String canonicalPath = bomFolder.getCanonicalPath(); + // // Relative name by replacing the root folder with "." - will look something like .\abc\def + // moduleRelativeName = Constants.DOT + canonicalPath.replaceFirst(Pattern.quote(topLevelFolder), Constants.EMPTY_STRING); + // } catch (Exception e) { + // logger.debug("Error getting path - {} ", e.getMessage()); + // } + // // making sure the module's folder was listed by "gradle projects" command + // if (!moduleRelativeName.isEmpty() && projectsList != null && !projectsList.contains(moduleRelativeName)) { + // logger.debug("Ignoring project at {} - because it was not listed by \"gradle projects\" command", moduleRelativeName); + // continue; + // } + List lines = getDependenciesTree(bomFileFolder, moduleName); + if (lines != null) { + List dependencies = collectDependencies(lines, bomFileFolder, bomFileFolder.equals(topLevelFolder), bomFile); + if (dependencies.size() > 0) { + AgentProjectInfo agentProjectInfo = new AgentProjectInfo(); + agentProjectInfo.getDependencies().addAll(dependencies); + if (!gradleAggregateModules) { + Coordinates coordinates = new Coordinates(); + coordinates.setArtifactId(moduleName); + agentProjectInfo.setCoordinates(coordinates); + } + projectInfoPathMap.put(agentProjectInfo, bomFolder.toPath()); + if (ignoreSourceCode) { + excludes.addAll(normalizeLocalPath(projectFolder, topLevelFolder, extensionPattern(GRADLE_SCRIPT_EXTENSION), null)); + } + } + } + } + topLevelFoldersNames.add(topLevelFolder.substring(topLevelFolder.lastIndexOf(fileSeparator) + 1)); + excludes.addAll(getExcludes()); + ResolutionResult resolutionResult; + if (!gradleAggregateModules) { + resolutionResult = new ResolutionResult(projectInfoPathMap, excludes, getDependencyType(), topLevelFolder); + } else { + resolutionResult = new ResolutionResult(projectInfoPathMap.keySet().stream() + .flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()), excludes, getDependencyType(), topLevelFolder); + } + logger.debug("total projects = {}",resolutionResult.getResolvedProjects().size()); + return resolutionResult; + } + + @Override + protected Collection getExcludes() { + Set excludes = new HashSet<>(); + for (String topLeverFolderName : topLevelFoldersNames) { + excludes.add(GLOB_PATTERN + topLeverFolderName + Constants.JAR_EXTENSION); + } + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return GRADLE_SCRIPT_EXTENSION; + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.GRADLE; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.GRADLE.name(); + } + + @Override + protected String[] getBomPattern() { + return new String[]{Constants.GLOB_PATTERN_PREFIX + Constants.BUILD_GRADLE}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(Constants.BUILD_GRADLE); + } + + @Override + protected Collection getLanguageExcludes() { + return null; + } + + /* --- Private methods --- */ + + private List getDependenciesTree(String directory, String directoryName) { + List lines; + if (dependencyTrees.get(directoryName) == null) { + String[] gradleCommandParams = gradleCli.getGradleCommandParams(GradleMvnCommand.DEPENDENCIES); + lines = gradleCli.runGradleCmd(directory, gradleCommandParams, true); + dependencyTrees.put(directoryName, lines); + } else { + lines = dependencyTrees.get(directoryName); + } + return lines; + } + + private List collectDependencies(List lines, String directory, boolean isParent, String bomFile) { + List dependencyInfos = new ArrayList<>(); + String directoryName = Constants.EMPTY_STRING; + if (!isParent) { + // get the name of the directory + String[] directoryPath = directory.split(Pattern.quote(fileSeparator)); + directoryName = directoryPath[directoryPath.length - 1]; + } + // get gradle dependencies, if the command runs successfully parse the dependencies + directoryName = fileSeparator.concat(directoryName); + dependencyInfos.addAll(gradleLinesParser.parseLines(lines, directory, directoryName, ignoredScopes, bomFile)); + return dependencyInfos; + } + + private List collectProjects(String rootDirectory) { + List projectsList = gradleCli.runGradleCmd(rootDirectory, gradleCli.getGradleCommandParams(GradleMvnCommand.PROJECTS), true); + List resultProjectsList = null; + if (projectsList != null) { + resultProjectsList = new ArrayList<>(); + for (String line : projectsList) { + if (line.contains(PROJECT)) { + // Relevant lines look like: + // | +--- Project ':nes:t4' - optional description for project + // | \--- Project ':nes:t5' - optional description for project + // +--- Project ':template-server3' + // Split the line + String[] lineParts = line.split(PROJECT); + if (lineParts.length == 2) { + String partWithNameAndDescription = lineParts[1].trim(); + String projectName; + // No description at the end of line + if (partWithNameAndDescription.endsWith(Constants.APOSTROPHE)) { + projectName = partWithNameAndDescription.trim().replaceAll(Constants.APOSTROPHE, Constants.EMPTY_STRING); + } else { + String[] projectAndDescription = partWithNameAndDescription.split(Constants.APOSTROPHE); + projectName = projectAndDescription[1]; + } + // Convert the project name to a path name + // Example: :abc:def --> .\abc\def + String projectNameAsPath = Constants.DOT + projectName; + projectNameAsPath = projectNameAsPath.replaceAll(Constants.COLON, Matcher.quoteReplacement(File.separator)); + resultProjectsList.add(projectNameAsPath); + } + } + } + } + return resultProjectsList; + } + + // copy all the bom files (build.gradle) to temp folder and run the command "gradle copyDependencies" + private void downloadMissingDependencies(String projectFolder) { + logger.debug("running pre-steps on folder {}", projectFolder); + String tempFolder = new FilesUtils().createTmpFolder(false, TempFolders.UNIQUE_GRADLE_TEMP_FOLDER); + File buildGradleTempDirectory = new File(tempFolder); + if (copyProjectFolder(projectFolder, buildGradleTempDirectory)) { + try { + Stream pathStream = Files.walk(Paths.get(buildGradleTempDirectory.getPath()), Integer.MAX_VALUE).filter(file -> file.getFileName().toString().equals(Constants.BUILD_GRADLE)); + pathStream.forEach(path -> { + File buildGradleTmp = new File(path.toString()); + if (buildGradleTmp.exists()) { + if (appendTaskToBomFile(buildGradleTmp)) { + runPreStepCommand(buildGradleTmp); + removeTaskFromBomFile(buildGradleTmp); + } + } else { + logger.warn("Could not find the path {}", buildGradleTmp.getPath()); + } + }); + } catch (IOException e) { + logger.warn("Couldn't list all 'build.gradle' files, error: {}", e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } finally { + new TempFolders().deleteTempFoldersHelper(Paths.get(System.getProperty("java.io.tmpdir"), TempFolders.UNIQUE_GRADLE_TEMP_FOLDER).toString()); + } + } + } + + // copy project to local temp directory + private boolean copyProjectFolder(String projectFolder, File buildGradleTempDirectory) { + try { + FileUtils.copyDirectory(new File(projectFolder), buildGradleTempDirectory); + } catch (IOException e) { + logger.error("Could not copy the folder {} to {} , the cause {}", projectFolder, buildGradleTempDirectory.getPath(), e.getMessage()); + return false; + } + logger.debug("copied folder {} to temp folder successfully", projectFolder); + return true; + } + + // append new task to bom file + private boolean appendTaskToBomFile(File buildGradleTmp) { + FileReader fileReader; + BufferedReader bufferedReader = null; + InputStream inputStream = null; + boolean hasDependencies = false; + try { + // appending the task only if the build.gradle file has 'dependencies {' node (only at the beginning of the line) + // otherwise, later when the task is ran it'll fail + fileReader = new FileReader(buildGradleTmp); + bufferedReader = new BufferedReader(fileReader); + String currLine; + while ((currLine = bufferedReader.readLine()) != null) { + if (currLine.indexOf(DEPENDENCIES + Constants.WHITESPACE + CURLY_BRACKETS_OPEN) == 0 || currLine.indexOf(DEPENDENCIES + CURLY_BRACKETS_OPEN) == 0) { + hasDependencies = true; + break; + } + } + if (hasDependencies) { + byte[] bytes; + List lines = getDependenciesTree(buildGradleTmp.getParent(), buildGradleTmp.getParentFile().getName()); + if (lines != null) { + List scopes = getScopes(lines); + String copyDependenciesTask = Constants.NEW_LINE + TASK_COPY_DEPENDENCIES_HEADER + Constants.NEW_LINE; + for (String scope : scopes) { + copyDependenciesTask = copyDependenciesTask.concat(" from configurations." + scope + Constants.NEW_LINE); + } + copyDependenciesTask = copyDependenciesTask.concat(TASK_COPY_DEPENDENCIES_FOOTER + Constants.NEW_LINE + CURLY_BRACKTES_CLOSE); + bytes = copyDependenciesTask.getBytes(); + } else { + ClassLoader classLoader = Main.class.getClassLoader(); + inputStream = classLoader.getResourceAsStream(COPY_DEPENDENCIES_TASK_TXT); + bytes = IOUtils.toByteArray(inputStream); + } + if (bytes.length > 0) { + Files.write(Paths.get(buildGradleTmp.getPath()), bytes, StandardOpenOption.APPEND); + } else if (lines == null) { + logger.warn("Could not read {}", COPY_DEPENDENCIES_TASK_TXT); + } else { + logger.warn("Could not read dependencies' tree"); + } + } + } catch (IOException e) { + logger.error("Could not write into the file {}, the cause {}", buildGradleTmp.getPath(), e.getMessage()); + hasDependencies = false; + } + try { + if (inputStream != null) { + inputStream.close(); + } + if (bufferedReader != null) { + bufferedReader.close(); + } + } catch (IOException e) { + logger.error("Could close the file, cause", e.getMessage()); + } + return hasDependencies; + } + + private List getScopes(List lines) { + List scopes = new ArrayList<>(); + for (int i = 1; i < lines.size(); i++) { + String currLine = lines.get(i); + String prevLine = lines.get(i - 1); + if ((currLine.startsWith(Constants.PLUS) || currLine.startsWith(Constants.BACK_SLASH)) && + (!prevLine.startsWith(Constants.WHITESPACE) && !prevLine.startsWith(Constants.PIPE) && !prevLine.startsWith(Constants.PLUS) && !prevLine.startsWith(Constants.BACK_SLASH))) { + String scope = prevLine.split(" - ")[0]; + scopes.add(scope); + } + } + return scopes; + } + + // run pre step command gradle copyDependencies + private void runPreStepCommand(File bomFile) { + String directory = bomFile.getParent(); + String[] gradleCommandParams = gradleCli.getGradleCommandParams(GradleMvnCommand.COPY_DEPENDENCIES); + if (StringUtils.isNotEmpty(directory) && gradleCommandParams.length > 0) { + gradleCli.runGradleCmd(directory, gradleCommandParams, true); + } else { + logger.warn("Could not run gradle command"); + } + } + + // there are cases where modules are connected to each other, and in such cases some scopes inside the copy-dependencies task interfere with other modules; + // therefore, after running the copy-dependencies task removing it from the build.gradle file + private void removeTaskFromBomFile(File buildGradleTmp) { + FileReader fileReader; + BufferedReader bufferedReader = null; + try { + fileReader = new FileReader(buildGradleTmp); + bufferedReader = new BufferedReader(fileReader); + String currLine; + String originalLines = ""; + while ((currLine = bufferedReader.readLine()) != null) { + if (currLine.equals(TASK_COPY_DEPENDENCIES_HEADER)) { + break; + } else { + originalLines = originalLines.concat(currLine + Constants.NEW_LINE); + } + } + if (!originalLines.isEmpty()) { + byte[] bytes = originalLines.getBytes(); + Files.write(Paths.get(buildGradleTmp.getPath()), bytes, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + } catch (IOException e) { + logger.warn("Couldn't remove 'copyDependencies' task from {}, error: {}", buildGradleTmp.getPath(), e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + } catch (IOException e) { + logger.error("Could close the file, cause", e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleLinesParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleLinesParser.java new file mode 100644 index 0000000..059efbd --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleLinesParser.java @@ -0,0 +1,499 @@ +package org.whitesource.agent.dependency.resolver.gradle; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.maven.MavenTreeDependencyCollector; +import org.whitesource.agent.utils.FilesScanner; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * This class represents parser for Maven output lines + * + * @author erez.huberman + */ +public class GradleLinesParser extends MavenTreeDependencyCollector { + protected static final String ARROW = " -> "; + protected static final String PROJECT = "project :"; + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(GradleLinesParser.class); + private static final String TMP_JAVA_FILE = "tmp.java"; + private static final String MAIN = "main"; + private static final String JAVA = "java"; + private static final String JAVA_EXTENSION = ".java"; + private static final String AAR_EXTENSION = ".aar"; + private static final String EXE_EXTENSION = ".exe"; + private static final String PLUS = "+---"; + private static final String SLASH = "\\---"; + private static final int INDENTETION_SPACE = 5; + private static final String JAR_EXTENSION = Constants.DOT + Constants.JAR; + private static final String ASTERIX = "(*)"; + public static final String[] GRADLE_PARSER_EXCLUDES = {"-sources"}; + + private String fileSeparator; + private String dotGradlePath; + private String rootDirectory; + private String directoryName; + private String prevRootDirectory; + private boolean runAssembleCommand; + private boolean dependenciesDownloadAttemptPerformed; + private GradleCli gradleCli; + private String javaDirPath; + private String mainDirPath; + private String srcDirPath; + private String gradleLocalRepositoryPath; + private boolean removeJavaDir; + private boolean removeMainDir; + private boolean removeSrcDir; + private boolean removeJavaFile; + private boolean mavenFound = true; + + GradleLinesParser(boolean runAssembleCommand, GradleCli gradleCli, String gradleLocalRepositoryPath) { + // send maven.runPreStep default value "false", irrelevant for gradle dependency resolution. (WSE-860) + super(null, true, false, false); + this.runAssembleCommand = runAssembleCommand; + this.gradleLocalRepositoryPath = gradleLocalRepositoryPath; + // using the same 'gradleCli' object as the GradleDependencyResolver for if the value of 'preferredEnvironment' changes, + // it should change for both GradleLInesParser and GradleDependencyResolver + this.gradleCli = gradleCli; + fileSeparator = System.getProperty(Constants.FILE_SEPARATOR); + srcDirPath = fileSeparator + Constants.SRC; + mainDirPath = srcDirPath + fileSeparator + MAIN; + javaDirPath = mainDirPath + fileSeparator + JAVA; + } + + public List parseLines(List lines, String rootDirectory, String directoryName, String[] ignoredScopes, String bomFile) { + if (StringUtils.isBlank(dotGradlePath)) { + this.dotGradlePath = getDotGradleFolderPath(); + } + if (this.dotGradlePath == null) { + return new ArrayList<>(); + } + this.rootDirectory = rootDirectory; + this.directoryName = directoryName; + logger.info("Start parsing gradle dependencies of: {}", rootDirectory + directoryName); + + //parse dependencies + //check if to ignore scopes and parse lines of gradle dependencies to map of scopes + if (ignoredScopes.length != 0) { + lines = ignoreScopesOfGradleDependencies(ignoredScopes, lines); + } + List projectsLines = lines.stream() + .filter(line -> (line.contains(PLUS) || line.contains(SLASH) || line.contains(Constants.PIPE)) && !line.contains(ASTERIX)) + .collect(Collectors.toList()); + List dependenciesList = new ArrayList<>(); + Stack parentDependencies = new Stack<>(); + List sha1s = new ArrayList<>(); + int prevLineIndentation = 0; + boolean duplicateDependency = false; + boolean insideProject = false; + for (String line : projectsLines) { + try { + if (line.indexOf(Constants.COLON) == -1 || line.contains(PROJECT)) { + if (line.contains(PROJECT)) { + /* + * there may be such scenarios - + \--- project :tapestry-ioc + +--- project :tapestry-func + +--- project :tapestry5-annotations + +--- project :plastic + | \--- org.slf4j:slf4j-api:1.7.25 + +--- project :beanmodel + | +--- org.antlr:antlr:3.5.2 + | | +--- org.antlr:antlr-runtime:3.5.2 + | | \--- org.antlr:ST4:4.0.8 + | | \--- org.antlr:antlr-runtime:3.5.2 + in such case - ignore the lines starting with 'project' but collect their transitive dependencies + and add them to the root of the dependencies' tree root + * */ + prevLineIndentation = 0; + insideProject = true; + } + continue; + } + + String[] strings = line.split(Constants.COLON); + String groupId = strings[0]; + int lastSpace = groupId.lastIndexOf(Constants.WHITESPACE); + groupId = groupId.substring(lastSpace + 1); + String artifactId, version; + if (strings.length == 2) { + artifactId = strings[1].split(ARROW)[0]; + version = strings[1].split(ARROW)[1]; + } else { + artifactId = strings[1]; + version = strings[2]; + if (version.contains(Constants.WHITESPACE)) { + if (version.contains(ARROW)) { + version = version.split(ARROW)[1]; + } else { + version = version.split(Constants.WHITESPACE)[0]; + } + } + } + + // Create dependencyInfo & calculate SHA1 + DependencyInfo currentDependency = new DependencyInfo(groupId, artifactId, version); + DependencyFile dependencyFile = getDependencySha1(currentDependency); + if (dependencyFile != null && !dependencyFile.getSha1().equals(Constants.EMPTY_STRING)) { + if (sha1s.contains(dependencyFile.getSha1())) + continue; + sha1s.add(dependencyFile.getSha1()); + currentDependency.setSha1(dependencyFile.getSha1()); + currentDependency.setSystemPath(dependencyFile.getFilePath()); + currentDependency.setFilename(dependencyFile.getFileName()); + currentDependency.setDependencyFile(bomFile); + String extension = FilesUtils.getFileExtension(dependencyFile.getFilePath()); + currentDependency.setType(extension); + } + currentDependency.setDependencyType(DependencyType.GRADLE); + + if (dependenciesList.contains(currentDependency)) { + duplicateDependency = true; + continue; + } + // In case the dependency is transitive/child dependency + if ((line.startsWith(Constants.WHITESPACE) || line.startsWith(Constants.PIPE)) && !insideProject) { + if (duplicateDependency || parentDependencies.isEmpty()) { + continue; + } + // Check if 2 dependencies are siblings (under the hierarchy level) + if (lastSpace == prevLineIndentation) { + parentDependencies.pop(); + + } else if (lastSpace < prevLineIndentation) { + // Find father dependency of current node + /*+--- org.webjars.npm:isurl:1.0.0 + | +--- org.webjars.npm:has-to-string-tag-x:[1.2.0,2) -> 1.4.1 + | | \--- org.webjars.npm:has-symbol-support-x:[1.4.1,2) -> 1.4.1 + | \--- org.webjars.npm:is-object:[1.0.1,2) -> 1.0.1 + */ + while (prevLineIndentation > lastSpace - INDENTETION_SPACE && !parentDependencies.isEmpty()) { + parentDependencies.pop(); + prevLineIndentation -= INDENTETION_SPACE; + } + } + + if (!parentDependencies.isEmpty()) { + parentDependencies.peek().getChildren().add(currentDependency); + } else { + // if - for some reason - this is a transitive dependency but the parent-dependencies stack is empty, + // add this dependency to the root of the tree + dependenciesList.add(currentDependency); + } + parentDependencies.push(currentDependency); + } else { + duplicateDependency = false; + insideProject = false; + dependenciesList.add(currentDependency); + parentDependencies.clear(); + parentDependencies.push(currentDependency); + } + prevLineIndentation = lastSpace; + } catch (Exception e) { + logger.warn("Couldn't parse line {}, error: {} - {}", line, e.getClass().toString(), e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } + } + + return dependenciesList; + } + + private List ignoreScopesOfGradleDependencies(String[] ignoredScopes, List lines) { + String scope = Constants.EMPTY_STRING; + Map gradleScopes = new HashMap<>(); + for (String line : lines) { + if (Character.isLetter(line.charAt(0))) { + int indexOfWhiteSpace = line.indexOf(' '); + // in case line is a single word take it.. else take the first word before whitespace + if (indexOfWhiteSpace == -1) { + scope = line; + } else { + scope = line.substring(0, indexOfWhiteSpace); + } + gradleScopes.put(scope, ""); + } else { + if (gradleScopes.containsKey(scope)) { + String strConcatinator = gradleScopes.get(scope); + strConcatinator = strConcatinator + line + "\n"; + gradleScopes.put(scope, strConcatinator); + } + + } + } + // remove ignoredScopes from scopes if exists + for (String ignoredScope : ignoredScopes) { + gradleScopes.remove(ignoredScope); + } + return Arrays.asList(gradleScopes.values().toString().split("\n")); + } + + private String getDotGradleFolderPath() { + String currentUsersHomeDir = System.getProperty(Constants.USER_HOME); + File dotGradle = Paths.get(currentUsersHomeDir, ".gradle", "caches", "modules-2", "files-2.1").toFile(); + + if (dotGradle.exists()) { + return dotGradle.getAbsolutePath(); + } + logger.error("Could not get .gradle path, dependencies information will not be send to WhiteSource server."); + return null; + } + + private DependencyFile getDependencySha1(DependencyInfo dependencyInfo) { + DependencyFile dependencyFile = getSha1FromGradleCache(dependencyInfo); + if (dependencyFile == null) { + // if dependency not found in .gradle cache - looking for it in .m2 cache + dependencyFile = getSha1FromM2(dependencyInfo); + if (dependencyFile == null || dependencyFile.getSha1().equals(Constants.EMPTY_STRING)) { + // if dependency not found in .m2 cache - running 'gradle assemble' command which should download the dependency to .grade cache + // making sure the download attempt is performed only once for a directory, otherwise there might be an infinite loop + if (!rootDirectory.concat(directoryName).equals(prevRootDirectory)) { + dependenciesDownloadAttemptPerformed = false; + } + if (!dependenciesDownloadAttemptPerformed && downloadDependencies()) { + dependencyFile = getDependencySha1(dependencyInfo); + } else { + dependencyFile = getSha1FromLocalRepo(dependencyInfo); + if (dependencyFile == null) { + logger.error("Couldn't find sha1 for " + dependencyInfo.getGroupId() + Constants.DOT + + dependencyInfo.getArtifactId() + Constants.DOT + dependencyInfo.getVersion()); + } + } + } + } + return dependencyFile; + } + + // get sha1 form local repository + private DependencyFile getSha1FromLocalRepo(DependencyInfo dependencyInfo) { + File localRepo = new File(gradleLocalRepositoryPath); + DependencyFile dependencyFile = null; + if (localRepo.exists() && localRepo.isDirectory()) { + String artifactId = dependencyInfo.getArtifactId(); + String version = dependencyInfo.getVersion(); + String dependencyName = artifactId + Constants.DASH + version; + logger.debug("Looking for " + dependencyName + " in {}", localRepo.getPath()); + dependencyFile = findDependencySha1(dependencyName, gradleLocalRepositoryPath); + } else { + logger.warn("Could not find path {}", localRepo.getPath()); + } + return dependencyFile; + } + + private DependencyFile getSha1FromGradleCache(DependencyInfo dependencyInfo) { + String groupId = dependencyInfo.getGroupId(); + String artifactId = dependencyInfo.getArtifactId(); + String version = dependencyInfo.getVersion(); + logger.debug("looking for " + groupId + "." + artifactId + "." + version + " in .gradle cache"); + String dependencyName = artifactId + Constants.DASH + version; + DependencyFile dependencyFile = null; + // gradle file path includes the sha1 + if (dotGradlePath != null) { + String pathToDependency = dotGradlePath.concat(fileSeparator + groupId + fileSeparator + artifactId + fileSeparator + version); + File dependencyFolder = new File(pathToDependency); + // parsing gradle file path, get file hash from its path. the dependency folder version contains + // 2 folders one for pom and another for the jar. Look for the one with the jar in order to get the sha1 + // .gradle\caches\modules-2\files-2.1\junit\junit\4.12\2973d150c0dc1fefe998f834810d68f278ea58ec + if (dependencyFolder.isDirectory()) { + dependencyFile = findDependencySha1(dependencyName, pathToDependency); + } + } + if (dependencyFile == null) { + logger.debug("Couldn't find sha1 for " + groupId + Constants.DOT + artifactId + Constants.DOT + version + " inside .gradle cache."); + } + return dependencyFile; + } + + private DependencyFile getSha1FromM2(DependencyInfo dependencyInfo) { + if (!mavenFound) + return null; + String groupId = dependencyInfo.getGroupId(); + String artifactId = dependencyInfo.getArtifactId(); + String version = dependencyInfo.getVersion(); + logger.debug("looking for " + groupId + "." + artifactId + "." + version + " in .m2 cache"); + DependencyFile dependencyFile = null; + if (StringUtils.isBlank(M2Path)) { + this.M2Path = getMavenM2Path(Constants.DOT); + if (M2Path == null) { + logger.debug("Couldn't find .m2 path - maven is not installed"); + mavenFound = false; + return null; + } + } + + String pathToDependency = M2Path.concat(fileSeparator + String.join(fileSeparator, groupId.split("\\.")) + + fileSeparator + artifactId + fileSeparator + version + + fileSeparator + artifactId + Constants.DASH + version + Constants.JAR_EXTENSION); + File file = new File(pathToDependency); + if (file.isFile()) { + String sha1 = getSha1(pathToDependency); + dependencyFile = new DependencyFile(sha1, file); + if (sha1.equals(Constants.EMPTY_STRING)) { + logger.debug("Couldn't calculate sha1 for " + groupId + Constants.DOT + artifactId + Constants.DOT + version + ". "); + } + } else { + logger.debug("Couldn't find sha1 for " + groupId + Constants.DOT + artifactId + Constants.DOT + version + " inside .m2 cache."); + } + return dependencyFile; + } + + private boolean downloadDependencies() { + dependenciesDownloadAttemptPerformed = true; + prevRootDirectory = rootDirectory.concat(directoryName); + if (runAssembleCommand) { + try { + logger.info("running 'gradle assemble' command"); + long creationTime = new Date().getTime(); // will be used later for removing temp files/folders + validateJavaFileExistence(); + String[] gradleCommandParams = gradleCli.getGradleCommandParams(GradleMvnCommand.ASSEMBLE); + List lines = gradleCli.runCmd(rootDirectory, gradleCommandParams); + //removeTempJavaFolder(); + String errors = FilesUtils.removeTempFiles(rootDirectory, creationTime); + if (!errors.isEmpty()) { + logger.error(errors); + } + if (!lines.isEmpty()) { + for (String line : lines) { + if (line.contains("BUILD SUCCESSFUL")) { + return true; + } + } + } + } catch (IOException e) { + logger.debug("Failed running 'gradle assemble' command, got exception: " + e.getMessage()); + } + } else { + logger.debug("Can't run 'gradle assemble' to download missing dependencies. Change 'gradle.runAssembleCommand' in the configuration file to 'true'"); + } + logger.error("Failed running 'gradle assemble' command"); + return false; + } + + private void validateJavaFileExistence() throws IOException { + // the 'gradle assemble' command, existence of a java file (even empty) inside 'src/main/java' is required. + // therefore, verifying the file and the path exist - if not creating it, and after running the assemble command removing the item that was added + String javaDirPath = rootDirectory + directoryName + this.javaDirPath; + File javaDir = new File(javaDirPath); + String srcDirPath = rootDirectory + directoryName + this.srcDirPath; + File srcDir = new File(srcDirPath); + removeSrcDir = false; + if (!srcDir.isDirectory()) { // src folder doesn't exist - create the whole tree + FileUtils.forceMkdir(javaDir); + logger.debug("no 'src' folder, created temp " + javaDirPath); + removeSrcDir = true; + } else { + String mainDirPath = rootDirectory + directoryName + this.mainDirPath; + File mainDir = new File(mainDirPath); + removeMainDir = false; + if (!mainDir.isDirectory()) { // main folder doesn't exist - create it with its sub-folder + FileUtils.forceMkdir(javaDir); + logger.debug("no 'src/main' folder, created temp " + javaDirPath); + removeMainDir = true; + } else { + removeJavaDir = false; + if (!javaDir.isDirectory()) { // java folder doesn't exist - create it + FileUtils.forceMkdir(javaDir); + logger.debug("no 'src/main/java' folder, created temp " + javaDirPath); + removeJavaDir = true; + } + } + } + removeJavaFile = false; + if (!javaFileExists(rootDirectory + directoryName + this.javaDirPath)) { // the java folder doesn't have any java file inside it - creating a temp file + File javaFile = new File(javaDirPath + fileSeparator + TMP_JAVA_FILE); + removeJavaFile = javaFile.createNewFile(); + logger.debug("no java file, created temp " + javaFile.getPath()); + } + } + + private boolean javaFileExists(String directoryName) { + File directory = new File(directoryName); + File[] fList = directory.listFiles(); + if (fList != null) { + for (File file : fList) { + if (file.isFile()) { + if (file.getName().endsWith(JAVA_EXTENSION)) + return true; + } else if (file.isDirectory()) { + return javaFileExists(file.getAbsolutePath()); + } + } + } + return false; + } + + private DependencyFile findDependencySha1(String dependencyName, String pathToDependency) { + FilesScanner filesScanner = new FilesScanner(); + DependencyFile dependencyFile = null; + String[] parserIncludes = new String[]{Constants.GLOB_PATTERN_PREFIX + dependencyName + JAR_EXTENSION, Constants.GLOB_PATTERN_PREFIX + dependencyName + EXE_EXTENSION, + Constants.GLOB_PATTERN_PREFIX + dependencyName + AAR_EXTENSION}; + String[] directoryContent = filesScanner.getDirectoryContent(pathToDependency, parserIncludes, GRADLE_PARSER_EXCLUDES, + true, false, false); + if (directoryContent.length != 0) { + for (String dependencyFileName : directoryContent) { + if (dependencyFileName.contains(dependencyName)) { + String sha1 = getSha1(pathToDependency + File.separator + dependencyFileName); + // try to find the first file match + if (StringUtils.isNotBlank(sha1)) { + dependencyFile = new DependencyFile(sha1, new File(pathToDependency + File.separator + dependencyFileName)); + break; + } else { + logger.debug("Couldn't find sha1 for " + pathToDependency + File.separator + dependencyFileName + " inside .gradle cache."); + } + } + } + } else { + logger.debug("Could not find the dependency {} in {}", dependencyName, pathToDependency); + } + return dependencyFile; + } + + // removing only the folders/ file that were created + private void removeTempJavaFolder() throws IOException { + if (removeJavaDir) { + FileUtils.forceDelete(new File(rootDirectory + directoryName + this.javaDirPath)); + } else if (removeMainDir) { + FileUtils.forceDelete(new File(rootDirectory + directoryName + this.mainDirPath)); + } else if (removeSrcDir) { + FileUtils.forceDelete(new File(rootDirectory + directoryName + this.srcDirPath)); + } else if (removeJavaFile) { + FileUtils.forceDelete(new File(rootDirectory + directoryName + javaDirPath + fileSeparator + TMP_JAVA_FILE)); + } + } + + private class DependencyFile { + private String sha1; + private String filePath; + private String fileName; + + public DependencyFile(String sha1, File file) { + this.sha1 = sha1; + this.filePath = file.getPath(); + this.fileName = file.getName(); + } + + public String getSha1() { + return sha1; + } + + public String getFilePath() { + return filePath; + } + + public String getFileName() { + return fileName; + } + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleMvnCommand.java b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleMvnCommand.java new file mode 100644 index 0000000..9d73200 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/gradle/GradleMvnCommand.java @@ -0,0 +1,24 @@ +package org.whitesource.agent.dependency.resolver.gradle; + +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.go.GoDependencyResolver; + +public enum GradleMvnCommand { + COPY_DEPENDENCIES(Constants.COPY_DEPENDENCIES), + DEPENDENCIES(Constants.DEPENDENCIES), + ASSEMBLE(GradleCli.GRADLE_ASSEMBLE), + LOCK(GoDependencyResolver.GRADLE_LOCK), + PROJECTS(GradleCli.GRADLE_PROJECTS), + GO_DEPENDENCIES(GoDependencyResolver.GO_DEPENDENCIES), + GO_LOCK(GoDependencyResolver.GRADLE_GO_LOCK); + + private String value; + + GradleMvnCommand(String value) { + this.value = value; + } + + public String getCommand() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/hex/HexDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/hex/HexDependencyResolver.java new file mode 100644 index 0000000..1d9c342 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/hex/HexDependencyResolver.java @@ -0,0 +1,447 @@ +package org.whitesource.agent.dependency.resolver.hex; + +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.hash.ChecksumUtils; +import org.whitesource.agent.utils.Cli; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class HexDependencyResolver extends AbstractDependencyResolver { + + private static final List HEX_SCRIPT_EXTENSION = Arrays.asList(".ex"); + private static final String MIX_EXS = "mix.exs"; + private static final String MIX_LOCK = "mix.lock"; + private static final String MIX = "mix"; + private static final String DEPS_GET = "deps.get"; + private static final String DEPS_TREE = "deps.tree"; + private static final String ACCENT = "`"; + private static final String HEX_REGEX = "\"(\\w+)\": \\{:hex, :\\w+, \"(\\d+\\.\\d+\\.\\d+(?:-\\w+(?:\\.\\w+)*)?(?:\\+\\w+)?)\", \"(\\w+)\""; + private static final String GIT_REGEX = "\"(\\w+)\": \\{:git, \"(https|http|):/\\/github.com\\/\\w+\\/\\w+.git\", \"(\\w+)\""; + private static final String TREE_REGEX = "\\s(\\w+)\\s(~>\\s(\\d+\\.\\d+(\\.\\d+)?(?:-\\w+(?:\\.\\w+)*)?(?:\\+\\w+)?))?"; + private static final String VERSION_REGEX = "(\\d+\\.\\d+(\\.\\d+)?(?:-\\w+(?:\\.\\w+)*)?(?:\\+\\w+)?)"; + public static final String TAR_EXTENSION = ".tar"; + private static final String GIT = ":git,"; + private static final String MODULE_START = "==>"; + private static final String APPS = "apps"; + private static final String LINUX_PIPE = "│"; + private static final String LINUX_CHAR_1 = "├"; + private static final String LINUX_CHAR_2 = "└"; + + private final Logger logger = LoggerFactory.getLogger(HexDependencyResolver.class); + private boolean ignoreSourceFiles; + private boolean runPreStep; + private boolean aggregateModules; + private Cli cli; + private String dotHexCachePath; + + public HexDependencyResolver(boolean ignoreSourceFiles, boolean runPreStep, boolean aggregateModules){ + this.ignoreSourceFiles = ignoreSourceFiles; + this.runPreStep = runPreStep; + this.aggregateModules = aggregateModules; + cli = new Cli(); + String currentUsersHomeDir = System.getProperty(Constants.USER_HOME); + File dotHexCache = Paths.get(currentUsersHomeDir, ".hex", "packages","hexpm").toFile(); + if (dotHexCache.exists()){ + dotHexCachePath = dotHexCache.getAbsolutePath(); + } + } + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + if (this.runPreStep){ + runPreStep(topLevelFolder); + } + Map projectInfoPathMap = new HashMap<>(); + File mixLock = new File (topLevelFolder + fileSeparator + MIX_LOCK); + if (mixLock.exists()){ + Collection excludes = new HashSet<>(); + HashMap dependencyInfoMap = parseMixLoc(mixLock); + HashMap> modulesMap = parseMixTree(topLevelFolder, dependencyInfoMap); + if (!modulesMap.isEmpty()){ + for (String moduleName : modulesMap.keySet()){ + if (modulesMap.get(moduleName).size() > 0) { + AgentProjectInfo agentProjectInfo = new AgentProjectInfo(); + agentProjectInfo.getDependencies().addAll(modulesMap.get(moduleName)); + if (!aggregateModules && (modulesMap.size() > 1 || !moduleName.equals(topLevelFolder))) { + Coordinates coordinates = new Coordinates(); + coordinates.setArtifactId(moduleName); + agentProjectInfo.setCoordinates(coordinates); + } + File bomFolder = new File(moduleName.equals(topLevelFolder) ? moduleName : topLevelFolder + + fileSeparator + APPS + fileSeparator + moduleName); + projectInfoPathMap.put(agentProjectInfo, bomFolder.toPath()); + } + } + if (ignoreSourceFiles) { + excludes.addAll(normalizeLocalPath(projectFolder, topLevelFolder, extensionPattern(HEX_SCRIPT_EXTENSION), null)); + } + } + } else { + logger.warn("Can't find {}", mixLock.getPath()); + } + Collection excludes = new HashSet<>(); + excludes.addAll(getExcludes()); + ResolutionResult resolutionResult; + if (!aggregateModules) { + resolutionResult = new ResolutionResult(projectInfoPathMap, excludes, getDependencyType(), topLevelFolder); + } else { + resolutionResult = new ResolutionResult(projectInfoPathMap.keySet().stream() + .flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()), excludes, getDependencyType(), topLevelFolder); + } + return resolutionResult; + } + + private void runPreStep(String folderPath){ + logger.info("running hex pre-step"); + List compileOutput = cli.runCmd(folderPath, cli.getCommandParams(MIX, DEPS_GET)); + if (compileOutput.isEmpty()) { + logger.warn("Can't run '{} {}'", MIX, DEPS_GET); + } + } + + // this method is public only for testing purposes + public HashMap> parseMixTree(String folderPath, HashMap dependencyInfoMap){ + logger.info("Hex - parsing mix tree"); + List lines = cli.runCmd(folderPath, cli.getCommandParams(MIX, DEPS_TREE)); + int currentLevel; + int prevLevel = 0; + boolean insideModule = false; + HashMap> modulesMap = new HashMap<>(); + List dependenciesList = new ArrayList<>(); + Stack parentDependencies = new Stack<>(); + Pattern treePattern = Pattern.compile(TREE_REGEX); + + Matcher matcher; + String moduleName = null; + if (lines != null){ + for (String line : lines){ + try { + if (line.startsWith(MODULE_START)) { + moduleName = line.split(Constants.WHITESPACE)[1]; + modulesMap.put(moduleName, new ArrayList<>()); + parentDependencies.clear(); + } else { + if (line.startsWith(Constants.PIPE) || line.startsWith(ACCENT) || line.startsWith(Constants.WHITESPACE) + || line.startsWith(LINUX_CHAR_1) || line.startsWith(LINUX_CHAR_2) || line.startsWith(LINUX_PIPE)) { + /** + - dependency's line starts with either |, ` or white-space in windows, + and ├ (LINUX_CHAR_1), │ (LINUX_PIPE) or └ (LINUX_CHAR_2) in linux + - each level is has 4 more spaces than its parent level, therefore by dividing the index of dash by 4 + to find the line's level + code example: + + WINDOWS + telemetry + |-- erlang_pmp ~> 0.1 (Hex package) + |-- dialyxir ~> 1.0.0-rc.1 (Hex package) + `-- ex_doc ~> 0.19 (Hex package) + |-- earmark ~> 1.1 (Hex package) + `-- makeup_elixir ~> 0.7 (Hex package) + |-- makeup ~> 0.5.0 (Hex package) + | `-- nimble_parsec ~> 0.2.2 (Hex package) + `-- nimble_parsec ~> 0.2.2 (Hex package) + LINUX + telemetry + ├── erlang_pmp ~> 0.1 (Hex package) + ├── dialyxir ~> 1.0.0-rc.1 (Hex package) + └── ex_doc ~> 0.19 (Hex package) + ├── earmark ~> 1.1 (Hex package) + └── makeup_elixir ~> 0.7 (Hex package) + ├── makeup ~> 0.5.0 (Hex package) + │ └── nimble_parsec ~> 0.2.2 (Hex package) + └── nimble_parsec ~> 0.2.2 (Hex package) + + **/ + if (DependencyCollector.isWindows()){ + currentLevel = (line.indexOf(Constants.DASH) - 1) / 4; + } else { + currentLevel = Math.max(line.indexOf(LINUX_CHAR_1), line.indexOf(LINUX_CHAR_2)) / 4; + } + matcher = treePattern.matcher(line); + if (matcher.find()) { + if (insideModule && currentLevel > 0) { + continue; + } + insideModule = false; + String name = matcher.group(1); + String version = matcher.group(3); + DependencyInfo dependencyInfo = dependencyInfoMap.get(name); + if (dependencyInfo != null) { + getSha1AndVersion(dependencyInfo, version); + if (currentLevel == prevLevel) { // siblings dependencies + if (!parentDependencies.isEmpty()) { + parentDependencies.pop(); + if (!parentDependencies.isEmpty()) { + addTransitiveDependency(parentDependencies.peek(), dependencyInfo); + } + } + if (parentDependencies.isEmpty()) { + (moduleName == null ? dependenciesList : modulesMap.get(moduleName)).add(dependencyInfo); + } + parentDependencies.push(dependencyInfo); + } else if (currentLevel > prevLevel) { // transitive dependency + if (!parentDependencies.isEmpty()) { + addTransitiveDependency(parentDependencies.peek(), dependencyInfo); + } + parentDependencies.push(dependencyInfo); + } else { // dependency with higher hierarchy level than previous one + while (prevLevel > currentLevel - 1 && !parentDependencies.isEmpty()) { + parentDependencies.pop(); + prevLevel--; + } + if (!parentDependencies.isEmpty()) { + addTransitiveDependency(parentDependencies.peek(), dependencyInfo); + } else { + (moduleName == null ? dependenciesList : modulesMap.get(moduleName)).add(dependencyInfo); // root dependency + } + parentDependencies.push(dependencyInfo); + } + } else if (modulesMap.keySet().contains(name)) { + insideModule = true; + parentDependencies.clear(); + } + prevLevel = currentLevel; + } + } + } + } catch (Exception e){ + logger.warn("Failed parsing line '{}', error: {}", line, e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } + } + } + if (modulesMap.isEmpty()){ + modulesMap.put(folderPath,dependenciesList); + } + return modulesMap; + } + + // this method is public only for testing purposes + public HashMap parseMixLoc(File mixLock){ + logger.info("Hex - parsing " + mixLock.getPath()); + HashMap dependencyInfoHashMap = new HashMap<>(); + FileReader fileReader; + BufferedReader bufferedReader; + /* + lines of the mix.lock file can be of 2 types - hex of git + "artificery": {:hex, :artificery, "0.2.6", "f602909757263f7897130cbd006b0e40514a541b148d366ad65b89236b93497a", [:mix], [], "hexpm"}, + "aruspex": {:git, "https://github.com/oyeb/aruspex.git", "5ca5ca6057b61b2bc19a58abd3a5a656c39d0249", [branch: "tweaks"]}, + + using regex to identify the name, version in case of hex, and commit id in case git + * */ + try { + Pattern hexPattern = Pattern.compile(HEX_REGEX); + Pattern gitPattern = Pattern.compile(GIT_REGEX); + Matcher matcher; + fileReader = new FileReader(mixLock); + bufferedReader = new BufferedReader(fileReader); + String currLine; + while ((currLine = bufferedReader.readLine()) != null) { + if (currLine.startsWith(Constants.WHITESPACE)) { + logger.debug("parsing line {}", currLine); + try { + DependencyInfo dependencyInfo = null; + String name = null; + if (currLine.contains(GIT)) { + matcher = gitPattern.matcher(currLine); + if (matcher.find()) { + name = matcher.group(1); + String commitId = matcher.group(3); + dependencyInfo = new DependencyInfo(); + dependencyInfo.setArtifactId(name); + dependencyInfo.setCommit(commitId); + }else { + logger.debug("Failed matching GIT pattern on this line"); + } + } else { + matcher = hexPattern.matcher(currLine); + if (matcher.find()) { + name = matcher.group(1); + String version = matcher.group(2); + String sha1 = getSha1(name, version); + if (sha1 == null) { + dependencyInfo = new DependencyInfo(); + } else { + dependencyInfo = new DependencyInfo(sha1); + } + dependencyInfo.setArtifactId(name); + dependencyInfo.setVersion(version); + dependencyInfo.setSystemPath(dotHexCachePath + fileSeparator + name + Constants.DASH + version + TAR_EXTENSION); + dependencyInfo.setFilename(name + Constants.DASH + version + TAR_EXTENSION); + } else { + logger.debug("Failed matching HEX pattern on this line"); + } + } + if (dependencyInfo != null) { + dependencyInfo.setDependencyFile(mixLock.getParent() + fileSeparator + MIX_EXS); + dependencyInfo.setDependencyType(DependencyType.HEX); + dependencyInfoHashMap.put(name, dependencyInfo); + } + } catch (Exception e){ + logger.warn("Failed parsing this line, error: {}", e.getMessage()); + logger.debug("Exception: {}", e.getStackTrace()); + } + } + } + } catch (FileNotFoundException e) { + logger.warn("Can't find {}, error: {}", mixLock.getPath(), e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } catch (IOException e) { + logger.warn("Can't parse {}, error: {}", mixLock.getPath(), e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + return dependencyInfoHashMap; + } + + private void getSha1AndVersion(DependencyInfo dependencyInfo, String version){ + String name = dependencyInfo.getArtifactId(); + if (dependencyInfo.getSha1() == null){ + String sha1 = null; + if (version != null) { + sha1 = getSha1(name, version); + } else { + File tarFile = getTarFile(name); + if (tarFile != null){ + try { + sha1 = ChecksumUtils.calculateSHA1(tarFile); + // extracting the version from the TAR file's name + Pattern versionPattern = Pattern.compile(VERSION_REGEX); + Matcher matcher = versionPattern.matcher(tarFile.getName()); + if (matcher.find()) { + version = matcher.group(1); + } + } catch (IOException e){ + logger.warn("Failed calculating SHA1 of {}", tarFile.getPath()); + logger.debug("Error: {}", e.getStackTrace()); + } + } + } + if (sha1 != null){ + dependencyInfo.setSha1(sha1); + } + } + if (version != null) { + dependencyInfo.setSystemPath(dotHexCachePath + fileSeparator + name + Constants.DASH + version + TAR_EXTENSION); + dependencyInfo.setFilename(name + Constants.DASH + version + TAR_EXTENSION); + if (dependencyInfo.getVersion() == null) { + dependencyInfo.setVersion(version); + } + } + } + + // this method is used when there's a known version + private String getSha1(String name, String version) { + if (dotHexCachePath == null || name == null || version == null){ + logger.warn("Can't calculate SHA1, missing information: .hex-cache = {}, name = {}, version = {}", dotHexCachePath, name, version); + return null; + } + File tarFile = new File(dotHexCachePath + fileSeparator + name + Constants.DASH + version + TAR_EXTENSION); + try { + return ChecksumUtils.calculateSHA1(tarFile); + } catch (IOException e) { + logger.warn("Failed calculating SHA1 of {}. Make sure HEX is installed", tarFile.getPath()); + logger.debug("Error: {}", e.getStackTrace()); + return null; + } + } + + // this method is used when there's no known version. in such case finding in the cache all the tar files with the relevant name + // and then finding the most recent one (according to the version) + private File getTarFile(String name) { + File hexCache = new File(dotHexCachePath); + File[] files = hexCache.listFiles(new HexFileNameFilter(name)); + if (files != null && files.length > 0) { + Arrays.sort(files, Collections.reverseOrder()); + return files[0]; + } + logger.warn("Couldn't find tar file of {}", name); + return null; + } + + private void addTransitiveDependency(DependencyInfo parentDependency, DependencyInfo childDependency){ + // adding transitive dependency after making sure the child is not the parent, the parent dependecy doesn't contain + // this child already, the child dependency doesn't contain the parent, and that the parent is not a descendant of the child + if (parentDependency != childDependency && !parentDependency.getChildren().contains(childDependency) && + !childDependency.getChildren().contains(parentDependency) && !isDescendant(parentDependency, childDependency)) { + parentDependency.getChildren().add(childDependency); + } + } + + // avoiding circular-dependencies + private boolean isDescendant(DependencyInfo parentDependency, DependencyInfo childDependency){ + for (DependencyInfo dependencyInfo : childDependency.getChildren()){ + if (dependencyInfo.equals(parentDependency)) + return true; + return isDescendant(dependencyInfo, childDependency); + } + return false; + } + + @Override + protected Collection getExcludes() { + Set excludes = new HashSet<>(); + if(ignoreSourceFiles) { + for (String hexExtension : HEX_SCRIPT_EXTENSION) { + excludes.add(Constants.PATTERN + hexExtension); + } + } + return excludes; + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.HEX; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.HEX.name(); + } + + @Override + protected String[] getBomPattern() { + return new String[]{Constants.PATTERN + MIX_EXS}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(MIX_EXS, MIX_LOCK); + } + + @Override + protected Collection getLanguageExcludes() { + return null; + } + + @Override + public Collection getSourceFileExtensions() { + return HEX_SCRIPT_EXTENSION; + } +} + +class HexFileNameFilter implements FilenameFilter { + private String fileName; + + HexFileNameFilter(String name){ + fileName = name; + } + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().startsWith(fileName) && name.endsWith(HexDependencyResolver.TAR_EXTENSION); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/html/HtmlDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/html/HtmlDependencyResolver.java new file mode 100644 index 0000000..f322153 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/html/HtmlDependencyResolver.java @@ -0,0 +1,223 @@ +package org.whitesource.agent.dependency.resolver.html; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import org.apache.commons.lang.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.DependencyInfoFactory; +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; + +import javax.ws.rs.core.MediaType; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by anna.rozin + */ +public class HtmlDependencyResolver extends AbstractDependencyResolver { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(HtmlDependencyResolver.class); + + public static final List htmlTypeExtensions = Arrays.asList(Constants.HTM, Constants.HTML, Constants.SHTML, + Constants.XHTML, Constants.JSP, Constants.ASP, Constants.DO, Constants.ASPX); + public static final String URL_PATH = "://"; + + public final String[] includesPattern = new String[htmlTypeExtensions.size()]; + private final Pattern patternOfFirstLetter = Pattern.compile("[a-zA-Z].*"); + private final Pattern patternOfLegitSrcUrl = Pattern.compile("<%.*%>"); + private Map urlResponseMap = new HashMap<>(); + + /* --- Constructors --- */ + + public HtmlDependencyResolver() { + int i = 0; + for (String extension : htmlTypeExtensions) { + this.includesPattern[i++] = Constants.PATTERN + Constants.DOT + extension; + } + } + + /* --- Overridden methods --- */ + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + Collection dependencies = new LinkedList<>(); + + for (String htmlFile : bomFiles) { + Document htmlFileDocument; + try { + // todo consider collect other tags - not ser only + htmlFileDocument = Jsoup.parse(new File(htmlFile), Constants.UTF8); + Elements script = htmlFileDocument.getElementsByAttribute(Constants.SRC); + // create list of links for .js files for each html file + List scriptUrls = new LinkedList<>(); + for (Element srcLink : script) { + String src = srcLink.attr(Constants.SRC); + if (src != null && isLegitSrcUrl(src)) { + String srcUrl = fixUrls(src); + if (srcUrl != null) { + scriptUrls.add(srcUrl); + } + } + } + dependencies.addAll(collectJsFilesAndCalcHashes(scriptUrls, htmlFile, this.urlResponseMap)); + } catch (IOException e) { + logger.debug("Cannot parse the html file: {}", htmlFile); + } + } + + // delete parent folder of HTML Resolver + try { + new TempFolders().deleteTempFoldersHelper(Paths.get(System.getProperty("java.io.tmpdir"), TempFolders.UNIQUE_HTML_TEMP_FOLDER).toString()); + } catch (Exception e) { + logger.debug("Failed to delete HTML Dependency Resolver Folder{}", e.getMessage()); + } + // check the type and excludes + return new ResolutionResult(dependencies, getExcludes(), getDependencyType(), topLevelFolder); + } + + private boolean isLegitSrcUrl(String srcUrl) { + // Remove parameters if JS is called with parameters + // For example: http://somexample.com/test.js?a=1&b=3 + if (srcUrl.contains("?")) { + String[] srcURLSplit = srcUrl.split("\\?"); + srcUrl = srcURLSplit[0]; + } + if (srcUrl.endsWith(Constants.JS_EXTENSION)) { + Matcher matcher = this.patternOfLegitSrcUrl.matcher(srcUrl); + if (!matcher.find()) { + return true; + } + } + return false; + } + + private List collectJsFilesAndCalcHashes(List scriptUrls, String htmlFilePath, Map urlResponseMap) { + List dependencies = new LinkedList<>(); + String body = null; + String tempFolder = new FilesUtils().createTmpFolder(false, TempFolders.UNIQUE_HTML_TEMP_FOLDER); + File tempFolderFile = new File(tempFolder); + String dependencyFileName = null; + if (tempFolder != null) { + for (String scriptUrl : scriptUrls) { + try { + if (urlResponseMap.containsKey(scriptUrl)) { + body = urlResponseMap.get(scriptUrl); + } else { + Client client = Client.create(); + WebResource webResource = client.resource(scriptUrl); + ClientResponse response = webResource.accept(MediaType.APPLICATION_JSON).get(ClientResponse.class); + if (response.getStatus() != 200) { + logger.debug("Could not reach the registry using the URL: {}.", scriptUrl); + } else { + logger.debug("Found a dependency in html file {}, URL: {}", htmlFilePath, scriptUrl); + body = response.getEntity(String.class); + + String fileName = scriptUrl.substring(scriptUrl.lastIndexOf(Constants.FORWARD_SLASH) + 1); + dependencyFileName = tempFolder + File.separator + fileName; + PrintWriter writer = new PrintWriter(dependencyFileName, Constants.UTF8); + if (writer != null) { + writer.println(body); + writer.close(); + } + DependencyInfoFactory dependencyInfoFactory = new DependencyInfoFactory(); + DependencyInfo dependencyInfo = dependencyInfoFactory.createDependencyInfo(tempFolderFile, fileName); + if (dependencyInfo != null) { + dependencies.add(dependencyInfo); + dependencyInfo.setSystemPath(htmlFilePath); + dependencyInfo.setDependencyFile(htmlFilePath); + } + } + } + } catch (IOException e) { + logger.debug("Failed writing to file {}", dependencyFileName); + } catch (Exception e) { + logger.debug("Could not reach the registry using the URL: {}.", scriptUrl); + } finally { + if (StringUtils.isNotBlank(scriptUrl)) { + urlResponseMap.put(scriptUrl, body); + } + } + } + } + FilesUtils.deleteDirectory(tempFolderFile); + return dependencies; + } + + private String fixUrls(String scriptUrl) { + if (scriptUrl.startsWith(Constants.HTTP) || scriptUrl.startsWith(Constants.HTTPS)) { + return scriptUrl; + } + Matcher matcher = this.patternOfFirstLetter.matcher(scriptUrl); + matcher.find(); + if (matcher.group(0) != null) { + return Constants.HTTP + URL_PATH + matcher.group(0); + } else { + return null; + } + } + + @Override + protected Collection getExcludes() { + return new ArrayList<>(); + } + + @Override + public Collection getSourceFileExtensions() { + return htmlTypeExtensions; + } + + @Override + protected DependencyType getDependencyType() { + return null; + } + + @Override + protected String getDependencyTypeName() { + return Constants.HTML.toUpperCase(); + } + + @Override + protected boolean printResolvedFolder() { + return false; + } + + @Override + public String[] getBomPattern() { + return includesPattern; + } + + @Override + public Collection getManifestFiles(){ + return htmlTypeExtensions; + } + + @Override + protected Collection getLanguageExcludes() { + return new ArrayList<>(); + } + + @Override + protected Collection getRelevantScannedFolders(Collection scannedFolders) { + // HTML resolver should scan all folders and should not remove any folder + return scannedFolders == null ? Collections.emptyList() : scannedFolders; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenDependencyResolver.java new file mode 100644 index 0000000..fe7ebb9 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenDependencyResolver.java @@ -0,0 +1,200 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.maven; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.BomFile; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.utils.AddDependencyFileRecursionHelper; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +/** + * Dependency Resolver for Maven projects. + * + * @author eugen.horovitz + */ +public class MavenDependencyResolver extends AbstractDependencyResolver { + + /* --- Static Members --- */ + + private static final String POM_XML = "pom.xml"; + private static final List JAVA_EXTENSIONS = Arrays.asList(".java", ".jar", ".war", ".ear", ".car", ".class", "pom.xml"); + private static final String TEST = String.join(File.separator, new String[]{Constants.SRC, "test"}); + private final String MAIN_FOLDER = "Main_Folder"; + private final boolean mavenAggregateModules; + private final boolean ignoreSourceFiles; + private final boolean mavenIgnoreDependencyTreeErrors; + private final boolean ignorePomModules; + + /* --- Constructor --- */ + + public MavenDependencyResolver(boolean mavenAggregateModules, String[] mavenIgnoredScopes, boolean ignoreSourceFiles, boolean ignorePomModules, boolean runPreStep,boolean mavenIgnoreDependencyTreeErrors) { + super(); + this.dependencyCollector = new MavenTreeDependencyCollector(mavenIgnoredScopes, ignorePomModules, runPreStep, mavenIgnoreDependencyTreeErrors); + this.bomParser = new MavenPomParser(ignorePomModules); + this.mavenAggregateModules = mavenAggregateModules; + this.ignoreSourceFiles = ignoreSourceFiles; + this.mavenIgnoreDependencyTreeErrors = mavenIgnoreDependencyTreeErrors; + this.ignorePomModules = ignorePomModules; + } + + /* --- Members --- */ + + private final MavenTreeDependencyCollector dependencyCollector; + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + // try to collect dependencies via 'mvn dependency tree and parse' + Collection projects = dependencyCollector.collectDependencies(topLevelFolder); + if (mavenIgnoreDependencyTreeErrors && dependencyCollector.isErrorsRunningDependencyTree()) { + collectDependenciesFromPomXml(bomFiles, projects); + } + List files = bomFiles.stream().map(bomParser::parseBomFile) + .filter(Objects::nonNull).filter(bom -> !bom.getLocalFileName().contains(TEST)) + .collect(Collectors.toList()); + // create excludes for .JAVA files upon finding MAVEN dependencies + Set excludes = new HashSet<>(); + + addDependencyFile(projects, files); + + Map projectInfoPathMap = projects.stream().collect(Collectors.toMap(projectInfo -> projectInfo, projectInfo -> { + // map each pom file to specific project + Optional folderPath = files.stream().filter(file -> projectInfo.getCoordinates().getArtifactId().equals(file.getName())).findFirst(); + if (folderPath.isPresent()) { + File topFolderFound = new File(folderPath.get().getLocalFileName()).getParentFile(); + + // in java do not remove anything since they are not the duplicates of the dependencies found + // discard other java files only if specified ( decenciesOnly = true) + if (ignoreSourceFiles) { + excludes.addAll(normalizeLocalPath(projectFolder, topFolderFound.toString(), extensionPattern(JAVA_EXTENSIONS), null)); + } + return topFolderFound.toPath(); + } else { + if (ignoreSourceFiles) { + excludes.addAll(normalizeLocalPath(projectFolder, topLevelFolder, extensionPattern(JAVA_EXTENSIONS), null)); + } + } + return Paths.get(topLevelFolder); + })); + + ResolutionResult resolutionResult; + if (!mavenAggregateModules) { + resolutionResult = new ResolutionResult(projectInfoPathMap, excludes, getDependencyType(), topLevelFolder); + } else { + resolutionResult = new ResolutionResult(projectInfoPathMap.keySet().stream() + .flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()), excludes, getDependencyType(), topLevelFolder); + } + return resolutionResult; + } + + private void addDependencyFile(Collection projects, List files) { + projects.stream().forEach(agentProjectInfo -> { + BomFile bomFile = files.stream().filter(b -> b.getName().equals(agentProjectInfo.getCoordinates().getArtifactId())).findFirst().orElse(null); + if (bomFile != null){ + // this code turn the dependencies tree recursively into a flat-list, + // so that each dependency has its dependencyFile set + agentProjectInfo.getDependencies().stream() + .flatMap(AddDependencyFileRecursionHelper::flatten) + .forEach(dependencyInfo -> dependencyInfo.setDependencyFile(bomFile.getLocalFileName())); + } + }); + } + + // when failing to read data from 'mvn dependency:tree' output - trying to read directly from POM files + private void collectDependenciesFromPomXml(Set bomFiles, Collection projects) { + MavenPomParser pomParser = new MavenPomParser(ignorePomModules); + List bomFileList = new LinkedList<>(); + HashMap bomArtifactPathMap = new HashMap<>(); + for (String bomFileName : bomFiles) { + BomFile bomfile = pomParser.parseBomFile(bomFileName); + bomFileList.add(bomfile); + bomArtifactPathMap.put(bomfile.getName(), bomFileName); + } + + for (AgentProjectInfo project : projects) { + //add dependencies from pom to the modules that didn't fail (or failed partially) + String pomLocationPerProject = bomArtifactPathMap.get(project.getCoordinates().getArtifactId()); + if(pomLocationPerProject != null) { + bomArtifactPathMap.remove(project.getCoordinates().getArtifactId()); + List dependencyInfoList = pomParser.parseDependenciesFromPomXml(pomLocationPerProject); + // making sure not to add duplication of already existing dependencies + project.getDependencies().addAll(dependencyInfoList.stream().filter(dependencyInfo -> project.getDependencies().contains(dependencyInfo) == false).collect(Collectors.toList())); + } + } + + for (String artifactId : bomArtifactPathMap.keySet()) { + for (BomFile missingProject : bomFileList) { + //if project was not created due to failure add its dependencies + if (artifactId.equals(missingProject.getName())) { + AgentProjectInfo projectInfo = new AgentProjectInfo(); + projectInfo.setCoordinates(new Coordinates(missingProject.getGroupId(), missingProject.getName(), missingProject.getVersion())); + projectInfo.getDependencies().addAll(pomParser.parseDependenciesFromPomXml(bomArtifactPathMap.get(missingProject.getName()))); + projects.add(projectInfo); + break; + } + } + } + } + + @Override + protected Collection getExcludes() { + Set excludes = new HashSet<>(); + excludes.addAll(getLanguageExcludes()); + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return JAVA_EXTENSIONS; + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.MAVEN; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.MAVEN.name(); + } + + @Override + public String[] getBomPattern() { + return new String[]{Constants.PATTERN + POM_XML}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(POM_XML); + } + + @Override + protected Collection getLanguageExcludes() { + return new HashSet<>(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenLinesParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenLinesParser.java new file mode 100644 index 0000000..728d257 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenLinesParser.java @@ -0,0 +1,94 @@ +package org.whitesource.agent.dependency.resolver.maven; + +import fr.dutra.tools.maven.deptree.core.InputType; +import fr.dutra.tools.maven.deptree.core.Node; +import fr.dutra.tools.maven.deptree.core.ParseException; +import fr.dutra.tools.maven.deptree.core.Parser; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * This class represents parser for Maven output lines + * + * @author eugen.horovitz + */ +public class MavenLinesParser { + + /* --- Static members --- */ + + private Logger logger = LoggerFactory.getLogger(MavenLinesParser.class); + private static final String MAVEN_DEPENDENCY_PLUGIN_TREE = "maven-dependency-plugin:"; + private static final String INFO = "[INFO] "; + private static final String UTF_8 = "UTF-8"; + private static final String DOWNLOAD = "Download"; + + public List parseLines(List lines) { + // We remove here also lines like this: [INFO] Downloading from central: https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/2.0.1/jsr305-2.0.1.pom + List> projectsLines = lines.stream().filter(line -> line.contains(INFO) && !line.startsWith(INFO + DOWNLOAD)) // && !line.contains(Character.toString(Constants.OPEN_BRACKET)) && !line.endsWith(Character.toString(Constants.CLOSE_BRACKET))) + .map(line -> line.replace(INFO, Constants.EMPTY_STRING)) + .collect(splitBySeparator(formattedLines -> formattedLines.contains(MAVEN_DEPENDENCY_PLUGIN_TREE))); + + logger.info("Start parsing pom files"); + List nodes = new ArrayList<>(); + projectsLines.forEach(singleProjectLines -> { + /* WSE-730 + WSE-747: filtering out all lines not starting with either +-, \- or |, or those containing colons but not with 4 elements, or containing '- (' or ending with ) + for example, those lines will be filtered out - + +- (commons-collections:commons-collections:jar:3.2.1:compile - omitted for conflict with 3.2.2) + ---------------------< com.wss.test:search-engine >--------------------- + */ + List currentBlock = singleProjectLines.stream().filter( + line -> (line.trim().startsWith(Constants.PLUS + Constants.DASH) || + line.trim().startsWith(Constants.BACK_SLASH + Constants.DASH) || + line.trim().startsWith(Constants.PIPE) || + line.split(Constants.COLON).length == 4) && + !line.contains(Constants.DASH + Constants.WHITESPACE + Constants.OPEN_BRACKET) && + (!line.endsWith(Character.toString(Constants.CLOSE_BRACKET)) || line.endsWith(Character.toString(Constants.CLOSE_BRACKET) + Character.toString(Constants.CLOSE_BRACKET))) + ).collect(Collectors.toList()); + + String mvnLines = String.join(System.lineSeparator(), currentBlock); + try (InputStream is = new ByteArrayInputStream(mvnLines.getBytes(StandardCharsets.UTF_8.name())); + Reader lineReader = new InputStreamReader(is, UTF_8)) { + Parser parser = InputType.TEXT.newParser(); + Node tree = parser.parse(lineReader); + if (tree != null) + nodes.add(tree); + } catch (UnsupportedEncodingException e) { + logger.warn("unsupportedEncoding error parsing output : {}", e.getMessage()); + logger.debug("unsupportedEncoding error parsing output : {}", e.getStackTrace()); + } catch (ParseException e) { + logger.warn("error parsing output : {} ", e.getMessage()); + logger.debug("error parsing output : {} ", e.getStackTrace()); + } catch (Exception e) { + // this can happen often - some parts of the output are not parsable + logger.warn("error parsing output : {}", e.getMessage()); + logger.debug("error parsing output : {} \n{}", e.getMessage(), mvnLines); + } + }); + return nodes; + } + + // so : 29095967 + private static Collector>, List>> splitBySeparator(Predicate sep) { + return Collector.of(() -> new ArrayList>(Arrays.asList(new ArrayList<>())), + (l, elem) -> { + if (sep.test(elem)) { + l.add(new ArrayList<>()); + } else l.get(l.size() - 1).add(elem); + }, + (l1, l2) -> { + l1.get(l1.size() - 1).addAll(l2.remove(0)); + l1.addAll(l2); + return l1; + }); + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenPomParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenPomParser.java new file mode 100644 index 0000000..941c4b1 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenPomParser.java @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.maven; + +import org.apache.commons.lang.StringUtils; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.BomFile; +import org.whitesource.agent.dependency.resolver.IBomParser; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.File; +import java.io.FileReader; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class represents an MAVEN pom.xml file. + * + * @author eugen.horovitz + */ +public class MavenPomParser extends MavenTreeDependencyCollector implements IBomParser { + public final String COULD_NOT_PARSE_POM_FILE = "Could not parse pom file "; + + /* --- Static members --- */ + private static final String VERSION_REGEX = "(\\d+\\.\\d+(\\.\\d+)?(?:-\\w+(?:\\.\\w+)*)?(?:\\+\\w+)?)"; + + private final Logger logger = LoggerFactory.getLogger(MavenPomParser.class); + private boolean ignorePomModules; + + /* --- Constructor --- */ + + public MavenPomParser(boolean ignorePomModules) { + reader = new MavenXpp3Reader(); + this.ignorePomModules = ignorePomModules; + } + + /* --- Members --- */ + + private final MavenXpp3Reader reader; + + @Override + public BomFile parseBomFile(String bomPath) { + Model model = getModel(bomPath); + if (model != null && model.getArtifactId() != null) { + return new BomFile(model.getGroupId(), model.getArtifactId(),model.getVersion(),bomPath); + } + return null; + } + + public List parseDependenciesFromPomXml(String bomPath) { + Model model = getModel(bomPath); + if (model != null && model.getArtifactId() != null && (!ignorePomModules || !model.getPackaging().equals(Constants.POM))) { + // ignoring POM modules if 'ignorePosModule=true' + List directDependencies = Collections.emptyList(); + List managementDependencies = Collections.emptyList(); + + if (model.getDependencyManagement() != null && model.getDependencyManagement().getDependencies() != null) { + managementDependencies = model.getDependencyManagement().getDependencies(); + } + if (model.getDependencies() != null) { + directDependencies = model.getDependencies(); + } + List dependencies = new LinkedList<>(); + dependencies.addAll(directDependencies); + dependencies.addAll(managementDependencies); + List dependenciesInfo = new LinkedList<>(); + //in case the 'properties' node contains version data - extract it to be used later + HashMap versionDependencyMap = new HashMap<>(); + String key, value; + for (Map.Entry versionDependency : model.getProperties().entrySet()) { + key = Constants.DOLLAR + Constants.OPEN_CURLY_BRACKET + String.valueOf(versionDependency.getKey()) + Constants.CLOSE_CURLY_BRACKET; + value = String.valueOf(versionDependency.getValue()); + if (!value.contains(Constants.DOLLAR)) { + versionDependencyMap.put(key, value); + } + } + Pattern versionPattern = Pattern.compile(VERSION_REGEX); + Matcher matcher; + for (Dependency dependency : dependencies) { + String version; + if (versionDependencyMap.containsKey(dependency.getVersion())){ + version = versionDependencyMap.get(dependency.getVersion()); + } else { + version = dependency.getVersion(); + } + // ignoring dependencies without version or not a valid version (e.g. ${dependency.alfresco-messaging-repo.version}) + if (version == null){ + continue; + } else { + matcher = versionPattern.matcher(version); + if (matcher.find() == false){ + continue; + } + } + + // extracting the dependency's JAR (or WAR, TGZ, ect) file + String shortName; + if (StringUtils.isBlank(dependency.getClassifier())) { + shortName = dependency.getArtifactId() + Constants.DASH + dependency.getVersion() + Constants.DOT + dependency.getType(); + } else { + String nodePackaging = dependency.getType(); + if (nodePackaging.equals(TEST_JAR)) { + nodePackaging = Constants.JAR; + } + shortName = dependency.getArtifactId() + Constants.DASH + dependency.getVersion() + Constants.DASH + dependency.getClassifier() + Constants.DOT + nodePackaging; + } + if (StringUtils.isBlank(M2Path)){ + this.M2Path = getMavenM2Path(Constants.DOT); + } + String filePath = Paths.get(M2Path, dependency.getGroupId().replace(Constants.DOT, File.separator), dependency.getArtifactId(), dependency.getVersion(), shortName).toString(); + if (new File(filePath).exists()) { + String sha1 = getSha1(filePath); + if (!sha1.isEmpty()) { + DependencyInfo dependencyInfo = new DependencyInfo(dependency.getGroupId(), dependency.getArtifactId(), version); + dependencyInfo.setDependencyType(DependencyType.MAVEN); + dependencyInfo.setScope(dependency.getScope()); + dependencyInfo.setType(dependency.getType()); + dependencyInfo.setSystemPath(filePath); + dependencyInfo.setSha1(sha1); + dependencyInfo.setDependencyFile(bomPath); + dependenciesInfo.add(dependencyInfo); + } + } + } + return dependenciesInfo; + } + return Collections.emptyList(); + } + + private Model getModel(String bomPath){ + Model model = null; + try { + try (FileReader fileReader = new FileReader(bomPath)) { + model = reader.read(fileReader); + } + } catch (Exception e) { + logger.debug(COULD_NOT_PARSE_POM_FILE + bomPath); + } + return model; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenTreeDependencyCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenTreeDependencyCollector.java new file mode 100644 index 0000000..9a96e6d --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/maven/MavenTreeDependencyCollector.java @@ -0,0 +1,324 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.maven; + +import fr.dutra.tools.maven.deptree.core.Node; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.hash.ChecksumUtils; +import org.whitesource.agent.utils.CommandLineProcess; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Collect dependencies using 'npm ls' or bower command. + * + * @author eugen.horovitz + */ +public class MavenTreeDependencyCollector extends DependencyCollector { + + /* --- Statics Members --- */ + + private static final Logger logger = LoggerFactory.getLogger(org.whitesource.agent.dependency.resolver.maven.MavenTreeDependencyCollector.class); + + private static final String MVN_PARAMS_M2PATH_PATH = "help:evaluate"; + private static final String MVN_PARAMS_M2PATH_LOCAL = "-Dexpression=settings.localRepository"; + private static final String MVN_PARAMS_TREE = "dependency:tree"; + private static final String MVN_COMMAND = "mvn"; + private static final String SCOPE_TEST = "test"; + private static final String SCOPE_PROVIDED = "provided"; + private static final String M2 = ".m2"; + private static final String REPOSITORY = "repository"; + public static final String ALL = "All"; + public static final String NONE = "None"; + public static final String EJB = "ejb"; + private final String B_PARAMETER = "-B"; + private final String VERSION_PARAMETER = "-v"; + protected final String TEST_JAR = "test-jar"; + private final String MVN_CLEAN = "clean"; + private final String MVN_INSTALL = "install"; + private final String MVN_SKIP_TESTS = "-DskipTests"; + private boolean errorsRunningDependencyTree = false; + + /* --- Members --- */ + + protected String M2Path; + private Set mavenIgnoredScopes; + private boolean showMavenTreeError; + private boolean ignorePomModules; + private boolean runPreStep; + private MavenLinesParser mavenLinesParser; + private boolean mavenIgnoreDependencyTreeErrors; + /* --- Constructors --- */ + + // this constructor was added only to allow MavenPomParser to extend this class + public MavenTreeDependencyCollector() { + } + + public MavenTreeDependencyCollector(String[] mavenIgnoredScopes, boolean ignorePomModules, boolean runPreStep, boolean mavenIgnoreDependencyTreeErrors) { + mavenLinesParser = new MavenLinesParser(); + this.mavenIgnoredScopes = new HashSet<>(); + if (mavenIgnoredScopes == null) { + this.mavenIgnoredScopes.add(SCOPE_PROVIDED); + this.mavenIgnoredScopes.add(SCOPE_TEST); + } else { + if (mavenIgnoredScopes.length == 1 && (mavenIgnoredScopes[0].equals(ALL) || mavenIgnoredScopes[0].equals(NONE))) { + // do not filter out any scope + } else { + Arrays.stream(mavenIgnoredScopes).filter(exclude -> StringUtils.isBlank(exclude)) + .map(exclude -> this.mavenIgnoredScopes.add(exclude)); + } + } + this.ignorePomModules = ignorePomModules; + this.runPreStep = runPreStep; + this.mavenIgnoreDependencyTreeErrors = mavenIgnoreDependencyTreeErrors; + } + + /* --- Public methods --- */ + + @Override + public Collection collectDependencies(String rootDirectory) { + Collection projects = new ArrayList<>(); + if (!this.isMavenExist(rootDirectory)) { + logger.warn("Please install maven"); + } else { + if (runPreStep) { + try { + CommandLineProcess mvnCleanInstall = new CommandLineProcess(rootDirectory, getCleanInstallCommandParams()); + mvnCleanInstall.executeProcess(); + if (mvnCleanInstall.isErrorInProcess()) { + logger.warn("Failed to execute the command {}", getCleanInstallCommandParams()); + } + + } catch (Exception e) { + logger.warn("Error while execute dependencies after running {} on {}, {}", getCleanInstallCommandParams(), rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + } + + if (StringUtils.isBlank(M2Path)) { + this.M2Path = getMavenM2Path(Constants.DOT); + } + + try { + CommandLineProcess mvnDependencies = new CommandLineProcess(rootDirectory, getLsCommandParamsBatchMode()); + List lines = mvnDependencies.executeProcess(); + + if (mvnDependencies.isErrorInProcess()) { + logger.debug("Failed to execute the command {}", getLsCommandParamsBatchMode()); + mvnDependencies = new CommandLineProcess(rootDirectory, getLsCommandParams()); + lines = mvnDependencies.executeProcess(); + } + // set flag of errors, in case we do not have errors we do not want to parse direct dependencies from pom later on. + if (mvnDependencies.isErrorInProcess()) { + this.errorsRunningDependencyTree = true; + } + if (!mvnDependencies.isErrorInProcess() || mavenIgnoreDependencyTreeErrors) { + List nodes = mavenLinesParser.parseLines(lines); + + logger.info("End parsing pom files , found : " + String.join(Constants.COMMA, + nodes.stream().map(node -> node.getArtifactId()).collect(Collectors.toList()))); + + projects = nodes.stream() + .filter(node -> !this.ignorePomModules || (ignorePomModules && !node.getPackaging().equals(Constants.POM))) + .map(tree -> { + Map> pathToDependenciesMap = new HashMap<>(); + List dependencies = new LinkedList<>(); + Stream nodeStream = tree.getChildNodes().stream().filter(node -> !mavenIgnoredScopes.contains(node.getScope())); + dependencies.addAll(nodeStream.map(node -> getDependencyFromNode(node, pathToDependenciesMap)).collect(Collectors.toList())); + Map pathToSha1Map = pathToDependenciesMap.keySet().stream().distinct().parallel().collect(Collectors.toMap(file -> file, file -> getSha1(file))); + pathToSha1Map.entrySet().forEach(pathSha1Pair -> pathToDependenciesMap.get(pathSha1Pair.getKey()).stream().forEach(dependency -> { + dependency.setSha1(pathSha1Pair.getValue()); + dependency.setSystemPath(pathSha1Pair.getKey()); + })); + AgentProjectInfo projectInfo = new AgentProjectInfo(); + projectInfo.setCoordinates(new Coordinates(tree.getGroupId(), tree.getArtifactId(), tree.getVersion())); + logger.debug("Project/Module coordinates: {}", projectInfo.getCoordinates().toString()); + logger.debug("Total project direct dependencies found : {}", dependencies.size()); + dependencies.stream().filter(dependency -> StringUtils.isNotEmpty(dependency.getSha1()) || + (StringUtils.isNotEmpty(dependency.getGroupId()) && StringUtils.isNotEmpty(dependency.getArtifactId()) + && StringUtils.isNotEmpty(dependency.getVersion()))).forEach(dependency -> + projectInfo.getDependencies().add(dependency)); + logger.debug("ProjectInfo direct dependency added : {}", projectInfo.getDependencies().size()); + return projectInfo; + }).collect(Collectors.toList()); + } else { + logger.warn("Failed to scan and send {}", getLsCommandParams()); //either dead code? supposed to be up there.. + } + } catch (IOException e) { + logger.warn("Error getting dependencies after running {} on {}, {}", getLsCommandParams(), rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + + if (projects != null && projects.isEmpty()) { + if (!showMavenTreeError) { + logger.warn("Failed to getting dependencies after running '{}'", getLsCommandParams()); + showMavenTreeError = true; + } + } + } + return projects; + } + + protected String getSha1(String filePath) { + try { + return ChecksumUtils.calculateSHA1(new File(filePath)); + } catch (IOException e) { + logger.warn("Failed getting " + filePath + ". Consider run 'mvn clean install' "); + return Constants.EMPTY_STRING; + } + } + + private boolean isMavenExist(String rootDirectory) { + try { + CommandLineProcess mvnProcess = new CommandLineProcess(rootDirectory, getVersionCommandParams()); + List lines = mvnProcess.executeProcess(); + if (mvnProcess.isErrorInProcess() || lines.isEmpty()) { + logger.debug("Failed to get maven version"); + return false; + } else { + logger.debug("Maven : {}", lines); + return true; + } + } catch (IOException io) { + logger.debug("Failed to get maven version : {}", io.getMessage()); + return false; + } + } + + private DependencyInfo getDependencyFromNode(Node node, Map> paths) { + logger.debug("converting node to dependency :" + node.getArtifactId()); + DependencyInfo dependency = new DependencyInfo(node.getGroupId(), node.getArtifactId(), node.getVersion()); + dependency.setDependencyType(DependencyType.MAVEN); + dependency.setScope(node.getScope()); + dependency.setType(node.getPackaging()); + + String shortName; + // in case of ejb packaging the short name should be jar file + String nodePackaging = EJB.equals(node.getPackaging()) ? Constants.JAR : node.getPackaging(); + if (StringUtils.isBlank(node.getClassifier())) { + shortName = dependency.getArtifactId() + Constants.DASH + dependency.getVersion() + Constants.DOT + nodePackaging; + } else { + if (nodePackaging.equals(TEST_JAR)) { + nodePackaging = Constants.JAR; + } + shortName = dependency.getArtifactId() + Constants.DASH + dependency.getVersion() + Constants.DASH + node.getClassifier() + Constants.DOT + nodePackaging; + } + String filePath = Paths.get(M2Path, dependency.getGroupId().replace(Constants.DOT, File.separator), dependency.getArtifactId(), dependency.getVersion(), shortName).toString(); + if (!paths.containsKey(filePath)) { + paths.put(filePath, new ArrayList<>()); + } + paths.get(filePath).add(dependency); + if (StringUtils.isNotBlank(filePath)) { + File jarFile = new File(filePath); + if (jarFile.exists()) { + dependency.setFilename(jarFile.getName()); + } + } + node.getChildNodes().forEach(childNode -> dependency.getChildren().add(getDependencyFromNode(childNode, paths))); + return dependency; + } + + /* --- Private methods --- */ + + private String[] getCleanInstallCommandParams() { + if (isWindows()) { + return new String[]{Constants.CMD, C_CHAR_WINDOWS, MVN_COMMAND, MVN_CLEAN, MVN_INSTALL, MVN_SKIP_TESTS}; + } else { + return new String[]{MVN_COMMAND, MVN_CLEAN, MVN_INSTALL, MVN_SKIP_TESTS}; + } + } + + private String[] getLsCommandParams() { + if (isWindows()) { + return new String[]{Constants.CMD, C_CHAR_WINDOWS, MVN_COMMAND, MVN_PARAMS_TREE}; + } else { + return new String[]{MVN_COMMAND, MVN_PARAMS_TREE}; + } + } + + private String[] getLsCommandParamsBatchMode() { + String[] commandParams = getLsCommandParams(); + String[] result = new String[commandParams.length + 1]; + for (int i = 0; i < commandParams.length; i++) { + result[i] = commandParams[i]; + } + result[result.length - 1] = B_PARAMETER; + return result; + } + + private String[] getVersionCommandParams() { + if (isWindows()) { + return new String[]{Constants.CMD, C_CHAR_WINDOWS, MVN_COMMAND, VERSION_PARAMETER}; + } else { + return new String[]{MVN_COMMAND, VERSION_PARAMETER}; + } + } + + protected String getMavenM2Path(String rootDirectory) { + String currentUsersHomeDir = System.getProperty(Constants.USER_HOME); + File m2Path = Paths.get(currentUsersHomeDir, M2, REPOSITORY).toFile(); + + if (m2Path.exists()) { + return m2Path.getAbsolutePath(); + } + String[] params = null; + if (isWindows()) { + params = new String[]{Constants.CMD, C_CHAR_WINDOWS, MVN_COMMAND, MVN_PARAMS_M2PATH_PATH, MVN_PARAMS_M2PATH_LOCAL}; + } else { + params = new String[]{MVN_COMMAND, MVN_PARAMS_M2PATH_PATH, MVN_PARAMS_M2PATH_LOCAL}; + } + try { + CommandLineProcess mvnProcess = new CommandLineProcess(rootDirectory, params); + List lines = mvnProcess.executeProcess(); + if (!mvnProcess.isErrorInProcess()) { + Optional pathLine = lines.stream().filter(line -> (new File(line).exists())).findFirst(); + if (pathLine.isPresent()) { + return pathLine.get(); + } else { + logger.warn("could not get m2 path : {} out: {}", rootDirectory, lines.stream().reduce(Constants.EMPTY_STRING, String::concat)); + showMavenTreeError = true; + return null; + } + } else { + logger.warn("Failed to scan and send {}", getLsCommandParams()); + return null; + } + } catch (IOException io) { + logger.warn("could not get m2 path : {}", io.getMessage()); + showMavenTreeError = true; + return null; + } + } + + + public boolean isErrorsRunningDependencyTree() { + return errorsRunningDependencyTree; + } + +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmBomParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmBomParser.java new file mode 100644 index 0000000..668a1a6 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmBomParser.java @@ -0,0 +1,130 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.npm; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.BomFile; +import org.whitesource.agent.dependency.resolver.BomParser; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * This class represents an NPM package.json file. + * + * @author eugen.horovitz + */ +public class NpmBomParser extends BomParser { + + /* --- Static members --- */ + + private static final String NPM_REGISTRY = "registry.npmjs.org"; + private static final String VISUALSTUDIO_REGISTRY = "pkgs.visualstudio"; + private static final String ARTIFACTORY_FORWARD_SLASH = "/artifactory/"; + private static String OPTIONAL_DEPENDENCIES = "optionalDependencies"; + private static String SHA1 = "_shasum"; + private static String NPM_PACKAGE_FORMAT = "{0}-{1}.tgz"; + private static String RESOLVED = "_resolved"; + + private final Logger logger = LoggerFactory.getLogger(NpmBomParser.class); + + /* --- Protected methods --- */ + + protected String getVersion(JSONObject json, String fileName) { + if (json.has(Constants.VERSION)) { + return json.getString(Constants.VERSION); + } + logger.debug("version not found in file {}", fileName); + return Constants.EMPTY_STRING; + } + + /* --- Static methods --- */ + + public static String getNpmArtifactId(String name, String version) { + return MessageFormat.format(NPM_PACKAGE_FORMAT, name, version); + } + + /* --- Overridden methods --- */ + + @Override + protected BomFile parseBomFile(String jsonText, String localFileName) { + JSONObject json = new JSONObject(jsonText); + String name = json.getString(Constants.NAME); + String version = getVersion(json, localFileName); + Map dependencies = getDependenciesFromJson(json, Constants.DEPENDENCIES); + Map optionalDependencies = getDependenciesFromJson(json, OPTIONAL_DEPENDENCIES); + String fileName = getFilename(name, version); + String sha1 = Constants.EMPTY_STRING; + String resolved = null; + if(json.has(RESOLVED)) { + resolved = json.getString(RESOLVED); + } + + // optional fields for packageJson parser + if (json.has(SHA1)) { + sha1 = json.getString(SHA1); + } else { + logger.debug("shasum not found in file {}", localFileName); + } + + RegistryType registryType = getRegistryType(resolved); + + BomFile bom = new BomFile(name, version, sha1, fileName, localFileName, dependencies, optionalDependencies, resolved, registryType); + return bom; + } + + private RegistryType getRegistryType(String resolved) { + RegistryType registryType = null; + if (StringUtils.isNotBlank(resolved)) { + if (resolved.contains(ARTIFACTORY_FORWARD_SLASH)) { + registryType = RegistryType.ARTIFACTORY; + } else if (resolved.contains(VISUALSTUDIO_REGISTRY)) { + registryType = RegistryType.VISUAL_STUDIO; + } else if (resolved.contains(NPM_REGISTRY)) { + registryType = RegistryType.NPM_REGISTRY; + } else { + registryType = RegistryType.OTHER; + } + } + return registryType; + } + + @Override + protected String getFilename(String name, String version) { + return getNpmArtifactId(name, version); + } + + /* --- Private methods --- */ + + private Map getDependenciesFromJson(JSONObject json, String keyJson) { + Map nameVersionMap = new HashMap<>(); + if (json.has(keyJson)) { + JSONObject optionals = json.getJSONObject(keyJson); + Iterator keys = optionals.keys(); + while (keys.hasNext()) { + String key = keys.next(); + nameVersionMap.put(key, optionals.getString(key)); + } + } + return nameVersionMap; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmDependencyResolver.java new file mode 100644 index 0000000..02a3c81 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmDependencyResolver.java @@ -0,0 +1,479 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.npm; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import org.eclipse.jgit.util.StringUtils; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.BomFile; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.dependency.resolver.bower.BowerDependencyResolver;; +import org.whitesource.agent.utils.AddDependencyFileRecursionHelper; +import org.whitesource.agent.utils.FilesScanner; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.fs.StatusCode; + +import javax.ws.rs.core.MediaType; +import java.io.File; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Dependency Resolver for NPM projects. + * + * @author eugen.horovitz + */ +public class NpmDependencyResolver extends AbstractDependencyResolver { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(NpmDependencyResolver.class); + + private static final String PACKAGE_JSON = "package.json"; + private static final String TYPE_SCRIPT_EXTENSION = ".ts"; + private static final String TSX_EXTENSION = ".tsx"; + private static final String JS_PATTERN = "**/*.js"; + private static final String EXAMPLE = "**/example/**/"; + private static final String EXAMPLES = "**/examples/**/"; + private static final String WS_BOWER_FOLDER = "**/.ws_bower/**/"; + private static final String TEST = "**/test/**/"; + private static final long NPM_DEFAULT_LS_TIMEOUT = 60; + private static final String VERSIONS = "versions"; + private static final String DIST = "dist"; + private static final String SHASUM = "shasum"; + + private static final String EXCLUDE_TOP_FOLDER = "node_modules"; + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer"; + private static final String BASIC = "Basic"; + private static final int NUM_THREADS = 8; + private static final String URL_SLASH = "%2F"; + + /* --- Members --- */ + + private final NpmLsJsonDependencyCollector bomCollector; + private final NpmBomParser bomParser; + private final boolean ignoreSourceFiles; + private final boolean runPreStep; + private final FilesScanner filesScanner; + private final String npmAccessToken; + private final boolean npmYarnProject; + + /* --- Constructor --- */ + + public NpmDependencyResolver(boolean includeDevDependencies, boolean ignoreSourceFiles, long npmTimeoutDependenciesCollector, + boolean runPreStep, boolean npmIgnoreNpmLsErrors, String npmAccessToken, boolean npmYarnProject, boolean ignoreScripts) { + super(); + bomCollector = npmYarnProject ? new YarnDependencyCollector(includeDevDependencies, npmTimeoutDependenciesCollector, ignoreSourceFiles, ignoreScripts) : new NpmLsJsonDependencyCollector(includeDevDependencies, npmTimeoutDependenciesCollector, npmIgnoreNpmLsErrors, ignoreScripts); + bomParser = new NpmBomParser(); + this.ignoreSourceFiles = ignoreSourceFiles; + this.runPreStep = runPreStep; + this.filesScanner = new FilesScanner(); + this.npmAccessToken = npmAccessToken; + this.npmYarnProject = npmYarnProject; + } + + public NpmDependencyResolver(boolean runPreStep, String npmAccessToken, boolean bowerIgnoreSourceFiles) { + this(false,bowerIgnoreSourceFiles, NPM_DEFAULT_LS_TIMEOUT , runPreStep, false, npmAccessToken, false, false); + } + + /* --- Overridden methods --- */ + + @Override + protected Collection getLanguageExcludes() { + // NPM can contain files generated by the WhiteSource Bower plugin + Set excludes = new HashSet<>(); + excludes.add(BowerDependencyResolver.WS_BOWER_FILE2); + excludes.add(BowerDependencyResolver.WS_BOWER_FILE1); + return excludes; + } + + @Override + public String[] getBomPattern() { + return new String[]{Constants.PATTERN + PACKAGE_JSON}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(PACKAGE_JSON); + } + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + if (runPreStep) { + getDependencyCollector().executePreparationStep(topLevelFolder); + String[] excludesArray = new String[getExcludes().size()]; + excludesArray = getExcludes().toArray(excludesArray); + String[] otherBomFiles = filesScanner.getDirectoryContent(topLevelFolder, getBomPattern(), excludesArray, false, false); + Arrays.stream(otherBomFiles).forEach(file -> bomFiles.add(Paths.get(topLevelFolder, file).toString())); + } + + logger.debug("Attempting to parse package.json files"); + // parse package.json files + Collection parsedBomFiles = new LinkedList<>(); + + Map> mapBomFiles = bomFiles.stream().map(file -> new File(file)).collect(Collectors.groupingBy(File::getParentFile)); + + List files = mapBomFiles.entrySet().stream().map(entry -> { + if (entry.getValue().size() > 1) { + return entry.getValue().stream().filter(this::fileShouldBeParsed).findFirst().get(); + } else { + return entry.getValue().stream().findFirst().get(); + } + }).collect(Collectors.toList()); + + files.forEach(bomFile -> { + BomFile parsedBomFile = getBomParser().parseBomFile(bomFile.getAbsolutePath()); + if (parsedBomFile != null && parsedBomFile.isValid()) { + parsedBomFiles.add(parsedBomFile); + } + }); + + logger.debug("Trying to collect dependencies via 'npm ls'"); + // try to collect dependencies via 'npm ls' + List bomFilesNames = parsedBomFiles.stream().map(BomFile::getLocalFileName).filter(s -> s.contains(EXCLUDE_TOP_FOLDER) == false).collect(Collectors.toList()); + Collection projects = getDependencyCollector().collectDependencies(topLevelFolder); + // in case there is more than one module (i.e. - many package.json files outside of node_modules folder) - collect their dependencies as well + bomFilesNames.stream().forEach(bomFilePath -> { + bomFilePath = bomFilePath.substring(0, bomFilePath.lastIndexOf(fileSeparator)); + if (bomFilePath.equals(topLevelFolder) == false){ + projects.addAll(getDependencyCollector().collectDependencies(bomFilePath)); + } + }); + + Collection dependencies = projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()); + // this code turn the dependencies tree recursively into a flat-list, + // so that each dependency has its dependencyFile set + dependencies.stream() + .flatMap(AddDependencyFileRecursionHelper::flatten) + .forEach(dependencyInfo -> dependencyInfo.setDependencyFile(projectFolder + fileSeparator + PACKAGE_JSON)); + + boolean lsSuccess = !getDependencyCollector().getNpmLsFailureStatus(); + // flag that indicates if the number of the dependencies is zero and npm ls succeeded + boolean zeroDependenciesList = false; + if (lsSuccess) { + logger.debug("'npm ls succeeded"); + if (!dependencies.isEmpty()) { + handleLsSuccess(parsedBomFiles, dependencies, npmAccessToken); + } else { + zeroDependenciesList = true; + } + } else { + logger.debug("'npm ls failed"); + dependencies.addAll(collectPackageJsonDependencies(parsedBomFiles)); + } + //removeDependenciesWithoutSha1(dependencies); + logger.debug("Creating excludes for .js files upon finding NPM dependencies"); + // create excludes for .js files upon finding NPM dependencies + List excludes = new LinkedList<>(); + if (!dependencies.isEmpty() || zeroDependenciesList) { + if (ignoreSourceFiles ) { + //return excludes.stream().map(exclude -> finalRes + exclude).collect(Collectors.toList()); + excludes.addAll(normalizeLocalPath(projectFolder, topLevelFolder, Arrays.asList(JS_PATTERN, Constants.PATTERN + TYPE_SCRIPT_EXTENSION, + Constants.PATTERN + TSX_EXTENSION), null)); + } else { + excludes.addAll(normalizeLocalPath(projectFolder, topLevelFolder, Arrays.asList(JS_PATTERN, Constants.PATTERN + TYPE_SCRIPT_EXTENSION, + Constants.PATTERN + TSX_EXTENSION), EXCLUDE_TOP_FOLDER)); + } + } + return new ResolutionResult(dependencies, excludes, getDependencyType(), topLevelFolder); + } + + @Override + protected Collection getExcludes() { + Set excludes = new HashSet<>(); + String bomPattern = getBomPattern()[0]; + excludes.add(EXAMPLE + bomPattern); + excludes.add(EXAMPLES + bomPattern); + excludes.add(WS_BOWER_FOLDER + bomPattern); + excludes.add(TEST + bomPattern); + + excludes.addAll(getLanguageExcludes()); + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return Arrays.asList(Constants.JS_EXTENSION, TYPE_SCRIPT_EXTENSION, TSX_EXTENSION); + } + + /* --- Protected methods --- */ + + // These methods are relevant only for npm and bower + + protected String getPreferredFileName() { + return PACKAGE_JSON; + } + + protected NpmBomParser getBomParser() { + return bomParser; + } + + protected DependencyType getDependencyType() { + return DependencyType.NPM; + } + + protected NpmLsJsonDependencyCollector getDependencyCollector() { + return bomCollector; + } + + protected boolean isMatchChildDependency(DependencyInfo childDependency, String name, String version) { + return childDependency.getArtifactId().equals(NpmBomParser.getNpmArtifactId(name, version)); + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.NPM.name(); + } + + protected void enrichDependency(DependencyInfo dependency, BomFile packageJson, String npmAccessToken) { + String sha1 = packageJson.getSha1(); + String registryPackageUrl = packageJson.getRegistryPackageUrl(); + if (StringUtils.isEmptyOrNull(sha1) && !StringUtils.isEmptyOrNull(registryPackageUrl)) { + sha1 = getSha1FromRegistryPackageUrl(registryPackageUrl, packageJson.isScopedPackage(), packageJson.getVersion(), packageJson.getRegistryType(), npmAccessToken); + } + dependency.setSha1(sha1); + dependency.setGroupId(packageJson.getName()); + dependency.setArtifactId(packageJson.getFileName()); + dependency.setVersion(packageJson.getVersion()); + dependency.setSystemPath(packageJson.getLocalFileName()); + dependency.setFilename(packageJson.getLocalFileName()); + dependency.setDependencyType(getDependencyType()); + } + + /* --- Private methods --- */ + + private String getSha1FromRegistryPackageUrl(String registryPackageUrl, boolean isScopeDep, String versionOfPackage, RegistryType registryType, String npmAccessToken) { + + String uriScopeDep = registryPackageUrl; + if (isScopeDep) { + try { + uriScopeDep = registryPackageUrl.replace(BomFile.DUMMY_PARAMETER_SCOPE_PACKAGE, URL_SLASH); + } catch (Exception e) { + logger.warn("Failed creating uri of {}", registryPackageUrl); + return Constants.EMPTY_STRING; + } + } + + + String responseFromRegistry = null; + try { + Client client = Client.create(); + ClientResponse response; + WebResource resource; + resource = client.resource(uriScopeDep); + if (StringUtils.isEmptyOrNull(npmAccessToken)) { + response = resource.accept(MediaType.APPLICATION_JSON).get(ClientResponse.class); + logger.debug("npm.accessToken is not defined"); + } else { + logger.debug("npm.accessToken is defined"); + if (registryType == RegistryType.VISUAL_STUDIO) { + String userCredentials = BEARER + Constants.COLON + npmAccessToken; + String basicAuth = BASIC + Constants.WHITESPACE + new String(Base64.getEncoder().encode(userCredentials.getBytes())); + response = resource.accept(MediaType.APPLICATION_JSON).header("Authorization", basicAuth).get(ClientResponse.class); + } else { + // Bearer authorization + String userCredentials = BEARER + Constants.WHITESPACE + npmAccessToken; + response = resource.accept(MediaType.APPLICATION_JSON).header("Authorization", userCredentials).get(ClientResponse.class); + } + } + if (response.getStatus() >= 200 && response.getStatus() < 300) { + responseFromRegistry = response.getEntity(String.class); + } else { + logger.debug("Got {} status code from registry using the url {}.", response.getStatus(), uriScopeDep); + } + } catch (Exception e) { + logger.warn("Could not reach the registry using the URL: {}. Got an error: {}", registryPackageUrl, e.getMessage()); + return Constants.EMPTY_STRING; + } + if (responseFromRegistry == null) { + return Constants.EMPTY_STRING; + } + JSONObject jsonRegistry = new JSONObject(responseFromRegistry); + String shasum; + if (isScopeDep) { + shasum = jsonRegistry.getJSONObject(VERSIONS).getJSONObject(versionOfPackage).getJSONObject(DIST).getString(SHASUM); + } else { + shasum = jsonRegistry.getJSONObject(DIST).getString(SHASUM); + } + return shasum; + } + + /** + * Collect dependencies from package.json files - without 'npm ls' + */ + private Collection collectPackageJsonDependencies(Collection packageJsons) { + Collection dependencies = new LinkedList<>(); + ConcurrentHashMap dependencyPackageJsonMap = new ConcurrentHashMap<>(); + ExecutorService executorService = Executors.newWorkStealingPool(NUM_THREADS); + Collection threadsCollection = new LinkedList<>(); + for (BomFile packageJson : packageJsons) { + if (packageJson != null && packageJson.isValid()) { + // do not add new dependencies if 'npm ls' already returned all + DependencyInfo dependency = new DependencyInfo(); + dependencies.add(dependency); + threadsCollection.add(new EnrichDependency(packageJson, dependency, dependencyPackageJsonMap, npmAccessToken)); + logger.debug("Collect package.json of the dependency in the file: {}", dependency.getFilename()); + } + } + runThreadCollection(executorService, threadsCollection); + logger.debug("set hierarchy of the dependencies"); + // remove duplicates dependencies + Map existDependencies = new HashMap<>(); + Map dependencyPackageJsonMapWithoutDuplicates = new HashMap<>(); + for (Map.Entry entry : dependencyPackageJsonMap.entrySet()) { + DependencyInfo keyDep = entry.getKey(); + String key = keyDep.getSha1() + keyDep.getVersion() + keyDep.getArtifactId(); + if (!existDependencies.containsKey(key)) { + existDependencies.put(key, keyDep); + dependencyPackageJsonMapWithoutDuplicates.put(keyDep, entry.getValue()); + } + } + setHierarchy(dependencyPackageJsonMapWithoutDuplicates, existDependencies); + return existDependencies.values(); + } + + private void runThreadCollection(ExecutorService executorService, Collection threadsCollection) { + try { + executorService.invokeAll(threadsCollection); + executorService.shutdown(); + } catch (InterruptedException e) { + logger.error("One of the threads was interrupted, please try to scan again the project. Error: {}", e.getMessage()); + System.exit(StatusCode.ERROR.getValue()); + } + } + + private boolean fileShouldBeParsed(File file) { + return (file.getAbsolutePath().endsWith(getPreferredFileName())); + } + + private void setHierarchy(Map dependencyPackageJsonMap, Map existDependencies) { + dependencyPackageJsonMap.forEach((dependency, packageJson) -> { + packageJson.getDependencies().forEach((name, version) -> { + Optional childDep = dependencyPackageJsonMap.keySet().stream() + .filter(childDependency -> isMatchChildDependency(childDependency, name, version)) + .findFirst(); + + if (childDep.isPresent()) { + DependencyInfo childDepGet = childDep.get(); + String key = childDepGet.getSha1() + childDepGet.getVersion() + childDepGet.getArtifactId(); + if (!existDependencies.containsKey(key)) { + dependency.getChildren().add(childDep.get()); + existDependencies.put(key, childDepGet); + } + } + }); + }); + } + + private void handleLsSuccess(Collection packageJsonFiles, Collection dependencies, String npmAccessToken) { + Map resultFiles = packageJsonFiles.stream() + .filter(packageJson -> packageJson != null && packageJson.isValid()) + .filter(distinctByKey(BomFile::getFileName)) + .collect(Collectors.toMap(BomFile::getUniqueDependencyName, Function.identity())); + + logger.debug("Handling all dependencies"); + Collection threadsCollection = new LinkedList<>(); + dependencies.forEach(dependency -> handleLSDependencyRecursivelyImpl(dependency, resultFiles, threadsCollection, npmAccessToken)); + ExecutorService executorService = Executors.newWorkStealingPool(NUM_THREADS); + runThreadCollection(executorService, threadsCollection); + } + + private void handleLSDependencyRecursivelyImpl(DependencyInfo dependency, Map resultFiles, Collection threadsCollection, String npmAccessToken) { + String uniqueName = BomFile.getUniqueDependencyName(dependency.getGroupId(), dependency.getVersion()); + BomFile packageJson = resultFiles.get(uniqueName); + if (packageJson != null) { + threadsCollection.add(new EnrichDependency(packageJson, dependency, npmAccessToken)); + } else { + logger.debug("Dependency {} could not be retrieved. 'package.json' could not be found", dependency.getArtifactId()); + } + logger.debug("handle the children dependencies in the file: {}", dependency.getFilename()); + dependency.getChildren().forEach(childDependency -> handleLSDependencyRecursivelyImpl(childDependency, resultFiles, threadsCollection, npmAccessToken)); + } + + // currently deprecated - not relevant + private void removeDependenciesWithoutSha1(Collection dependencies){ + Collection childDependencies = new ArrayList<>(); + for (Iterator iterator = dependencies.iterator(); iterator.hasNext();){ + DependencyInfo dependencyInfo = iterator.next(); + if (dependencyInfo.getSha1().isEmpty()){ + childDependencies.addAll(dependencyInfo.getChildren()); + iterator.remove(); + } + } + dependencies.addAll(childDependencies); + } + + private Predicate distinctByKey(Function keyExtractor) { + Map seen = new ConcurrentHashMap<>(); + return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } + + /* --- Nested classes --- */ + + class EnrichDependency implements Callable { + + /* --- Members --- */ + + private BomFile packageJson; + private DependencyInfo dependency; + private ConcurrentHashMap dependencyPackageJsonMap; + private String npmAccessToken; + + /* --- Constructors --- */ + + public EnrichDependency(BomFile packageJson, DependencyInfo dependency, String npmAccessToken) { + this.packageJson = packageJson; + this.dependency = dependency; + this.dependencyPackageJsonMap = null; + this.npmAccessToken = npmAccessToken; + } + + public EnrichDependency(BomFile packageJson, DependencyInfo dependency, + ConcurrentHashMap dependencyPackageJsonMap, String npmAccessToken) { + this.packageJson = packageJson; + this.dependency = dependency; + this.dependencyPackageJsonMap = dependencyPackageJsonMap; + this.npmAccessToken = npmAccessToken; + } + + /* --- Overridden methods --- */ + + @Override + public Void call() { + enrichDependency(this.dependency, this.packageJson, this.npmAccessToken); + if (dependencyPackageJsonMap != null) { + dependencyPackageJsonMap.putIfAbsent(this.dependency, this.packageJson); + } + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmLsJsonDependencyCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmLsJsonDependencyCollector.java new file mode 100644 index 0000000..0c64d21 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/npm/NpmLsJsonDependencyCollector.java @@ -0,0 +1,320 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.npm; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.utils.CommandLineProcess; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Collect dependencies using 'npm ls' or bower command. + * + * @author eugen.horovitz + */ +public class NpmLsJsonDependencyCollector extends DependencyCollector { + + /* --- Statics Members --- */ + + private final Logger logger = LoggerFactory.getLogger(NpmLsJsonDependencyCollector.class); + + public static final String LS_COMMAND = "ls"; + public static final String LS_PARAMETER_JSON = "--json"; + + private static final String NPM_COMMAND = isWindows() ? "npm.cmd" : "npm"; + private static final String RESOLVED = "resolved"; + private static final String LS_ONLY_PROD_ARGUMENT = "--only=prod"; + public static final String PEER_MISSING = "peerMissing"; + private static final String DEDUPED = "deduped"; + private static final String REQUIRED = "required"; + private static final String IGNORE_SCRIPTS = "--ignore-scripts"; + // Could be used for "-- UNMET DEPENDENCY" or "-- UNMET OPTIONAL DEPENDENCY" + private static final String UNMET_DEPENDENCY = "-- UNMET "; + private final Pattern GENERAL_PACKAGE_NAME_PATTERN = Pattern.compile(".* (.*)@(\\^)?[0-9]+\\.[0-9]+"); + private final Pattern SECOND_GENERAL_PACKAGE_PATTERN = Pattern.compile(".* (.*)@"); + + /* --- Members --- */ + + protected final boolean includeDevDependencies; + protected final boolean ignoreNpmLsErrors; + private boolean showNpmLsError; + protected boolean npmLsFailureStatus; + protected final long npmTimeoutDependenciesCollector; + private final boolean ignoreScripts; + + /* --- Constructors --- */ + + public NpmLsJsonDependencyCollector(boolean includeDevDependencies, long npmTimeoutDependenciesCollector, boolean ignoreNpmLsErrors, boolean ignoreScripts) { + this.npmTimeoutDependenciesCollector = npmTimeoutDependenciesCollector; + this.includeDevDependencies = includeDevDependencies; + this.ignoreNpmLsErrors = ignoreNpmLsErrors; + this.ignoreScripts = ignoreScripts; + this.npmLsFailureStatus = false; + } + + /* --- Public methods --- */ + + @Override + public Collection collectDependencies(String rootDirectory) { + Collection dependencies = new ArrayList<>(); + try { + CommandLineProcess npmLsJson = new CommandLineProcess(rootDirectory, getLsCommandParamsJson()); + npmLsJson.setTimeoutReadLineSeconds(this.npmTimeoutDependenciesCollector); + List linesOfNpmLsJson = npmLsJson.executeProcess(); + // flag that indicates if the 'npm ls' command failed + this.npmLsFailureStatus = npmLsJson.isErrorInProcess() && !this.ignoreNpmLsErrors; + StringBuilder json = new StringBuilder(); + for (String line : linesOfNpmLsJson) { + json.append(line); + } + if (json != null && json.length() > 0 && (!npmLsJson.isErrorInProcess() || this.ignoreNpmLsErrors)) { + logger.debug("'npm ls' output is not empty"); + if(npmLsJson.isErrorInProcess() && this.ignoreNpmLsErrors) { + logger.info("Ignore errors of 'npm ls'"); + } + getDependencies(new JSONObject(json.toString()), rootDirectory, dependencies); + } + } catch (IOException e) { + this.npmLsFailureStatus = true; + logger.warn("Error getting dependencies after running 'npm ls --json' on {}, error : {}", rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + + if (dependencies.isEmpty()) { + if (!showNpmLsError && this.npmLsFailureStatus) { + logger.warn("Failed to getting dependencies after running '{}', run {} on {} folder", getLsCommandParams(), getInstallParams(), rootDirectory); + showNpmLsError = true; + } + } + return getSingleProjectList(dependencies); + } + + public boolean executePreparationStep(String folder) { + String[] command = getInstallParams(); + logger.debug("Running install command : " + command); + CommandLineProcess npmInstall = new CommandLineProcess(folder, command); + try { + npmInstall.executeProcessWithoutOutput(); + } catch (IOException e) { + logger.debug("Could not run " + command + " in folder " + folder); + return false; + } + return npmInstall.isErrorInProcess(); + } + + /* --- Private methods --- */ + + private int getDependencies(JSONObject npmLsJson, List linesOfNpmLs, int currentLineNumber, Collection dependencies) { + if (npmLsJson.has(Constants.DEPENDENCIES)) { + JSONObject dependenciesJsonObject = npmLsJson.getJSONObject(Constants.DEPENDENCIES); + if (dependenciesJsonObject != null) { + for (int i = 0; i < dependenciesJsonObject.keySet().size(); i++) { + String currentLine = linesOfNpmLs.get(currentLineNumber); + // Examples: + // +-- UNMET DEPENDENCY @material-ui/core@1.5.0 + // | +-- UNMET DEPENDENCY @babel/runtime@7.0.0-beta.42 + // | | `-- UNMET DEPENDENCY regenerator-runtime@0.11.1 + // | | +-- UNMET OPTIONAL DEPENDENCY are-we-there-yet@1.1.4 + // | | | +-- UNMET OPTIONAL DEPENDENCY delegates@1.0.0 + // | | | `-- UNMET OPTIONAL DEPENDENCY readable-stream@2.3.6 + if (currentLine.endsWith(DEDUPED) || currentLine.contains(UNMET_DEPENDENCY)) { + currentLineNumber++; + continue; + } + String dependencyAlias = getTheNextPackageNameFromNpmLs(currentLine); + try { + JSONObject dependencyJsonObject = dependenciesJsonObject.getJSONObject(dependencyAlias); + DependencyInfo dependency = getDependency(dependencyAlias, dependencyJsonObject); + if (dependency != null) { + dependencies.add(dependency); + logger.debug("Collect child dependencies of {}", dependencyAlias); + // collect child dependencies + Collection childDependencies = new ArrayList<>(); + currentLineNumber = getDependencies(dependencyJsonObject, linesOfNpmLs, currentLineNumber + 1, childDependencies); + dependency.getChildren().addAll(childDependencies); + } else { + // it can be only if was an error in 'npm ls' + if (dependencyJsonObject.has(REQUIRED)) { + Object requiredObject = dependencyJsonObject.get(REQUIRED); + if (requiredObject instanceof JSONObject) { + currentLineNumber = getDependencies((JSONObject) requiredObject, linesOfNpmLs, currentLineNumber + 1, new ArrayList<>()); + } else { + currentLineNumber++; + } + } else { + currentLineNumber++; + } + } + } catch (JSONException e){ + if (ignoreNpmLsErrors) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + currentLineNumber++; + } else { + throw e; + } + } + } + } + } + return currentLineNumber; + } + + protected String getTheNextPackageNameFromNpmLs(String line) { + String result = null; + Matcher matcher = this.GENERAL_PACKAGE_NAME_PATTERN.matcher(line); + if (matcher.find()) { + // take only the name of the package from the match + result = matcher.group(1); + } + if (result == null) { + matcher = this.SECOND_GENERAL_PACKAGE_PATTERN.matcher(line); + if (matcher.find()) { + result = matcher.group(1); + } + } + return result; + } + + private String getVersionFromLink(String linkResolved) { + URI uri = null; + try { + uri = new URI(linkResolved); + } catch (URISyntaxException e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } + String path = uri.getPath(); + String idStr = path.substring(path.lastIndexOf('/') + 1); + int lastIndexOfDash = idStr.lastIndexOf(Constants.DASH); + int lastIndexOfDot = idStr.lastIndexOf(Constants.DOT); + String resultVersion = idStr.substring(lastIndexOfDash + 1, lastIndexOfDot); + return resultVersion; + } + + /* --- Protected methods --- */ + + protected void getDependencies(JSONObject jsonObject, String rootDirectory, Collection dependencies) { + try { + CommandLineProcess npmLs = new CommandLineProcess(rootDirectory, getLsCommandParams()); + npmLs.setTimeoutReadLineSeconds(this.npmTimeoutDependenciesCollector); + List linesOfNpmLs = npmLs.executeProcess(); + getDependencies(jsonObject, linesOfNpmLs, 1, dependencies); + } catch (IOException e) { + logger.warn("Error getting dependencies after running 'npm ls --json' on {}, error : {}", rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + } + + protected String[] getInstallParams() { + if (this.ignoreScripts) { + return new String[]{NPM_COMMAND, Constants.INSTALL, IGNORE_SCRIPTS}; + } else { + return new String[]{NPM_COMMAND, Constants.INSTALL}; + } + } + + protected String[] getLsCommandParams() { + if (includeDevDependencies) { + return new String[]{NPM_COMMAND, LS_COMMAND}; + } else { + return new String[]{NPM_COMMAND, LS_COMMAND, LS_ONLY_PROD_ARGUMENT}; + } + } + + protected String[] getLsCommandParamsJson() { + if (includeDevDependencies) { + return new String[]{NPM_COMMAND, LS_COMMAND, LS_PARAMETER_JSON}; + } else { + return new String[]{NPM_COMMAND, LS_COMMAND, LS_ONLY_PROD_ARGUMENT, LS_PARAMETER_JSON}; + } + } + + protected DependencyInfo getDependency(String dependencyAlias, JSONObject jsonObject) { + String name = dependencyAlias; + String version; + if (jsonObject.has(Constants.VERSION)) { + version = jsonObject.getString(Constants.VERSION); + } else { + if (jsonObject.has(RESOLVED)) { + version = getVersionFromLink(jsonObject.getString(RESOLVED)); + } else if (jsonObject.has(Constants.MISSING) && jsonObject.getBoolean(Constants.MISSING)) { + logger.warn("Unmet dependency --> {}", name); + return null; + } else if (jsonObject.has(PEER_MISSING) && jsonObject.getBoolean(PEER_MISSING)) { + logger.warn("Unmet dependency --> peer missing {}", name); + return null; + } else { + // we still should return null since this is a non valid dependency + logger.warn("Unknown error. 'version' tag could not be found for {}", name); + return null; + } + } + + String filename = NpmBomParser.getNpmArtifactId(name, version); + DependencyInfo dependency = new DependencyInfo(); + dependency.setGroupId(name); + dependency.setArtifactId(filename); + dependency.setVersion(version); + dependency.setFilename(filename); + dependency.setDependencyType(DependencyType.NPM); + return dependency; + } + + public boolean getNpmLsFailureStatus() { + return this.npmLsFailureStatus; + } + + /* --- Nested classes --- */ + + class ReadLineTask implements Callable { + + /* --- Members --- */ + + private final BufferedReader reader; + + /* --- Constructors --- */ + + ReadLineTask(BufferedReader reader) { + this.reader = reader; + } + + /* --- Overridden methods --- */ + + @Override + public String call() throws Exception { +// while (true) { } + return reader.readLine(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/npm/RegistryType.java b/src/main/java/org/whitesource/agent/dependency/resolver/npm/RegistryType.java new file mode 100644 index 0000000..67f8d3c --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/npm/RegistryType.java @@ -0,0 +1,11 @@ +package org.whitesource.agent.dependency.resolver.npm; + +/** + * @author raz.nitzan + */ +public enum RegistryType { + NPM_REGISTRY, + VISUAL_STUDIO, + ARTIFACTORY, + OTHER +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/npm/YarnDependencyCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/npm/YarnDependencyCollector.java new file mode 100644 index 0000000..8ca59bd --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/npm/YarnDependencyCollector.java @@ -0,0 +1,196 @@ +package org.whitesource.agent.dependency.resolver.npm; + +import org.apache.commons.io.IOUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.utils.CommandLineProcess; + +import java.io.*; +import java.util.*; + +public class YarnDependencyCollector extends NpmLsJsonDependencyCollector { + protected static final String SUCCESS = "success"; + protected static final String RESOLVED = "resolved"; + protected static final String TGZ = ".tgz"; + protected static final String OPTIONAL_DEPENDENCIES = "optionalDependencies"; + protected static final String AT = "@"; + protected static final String NODE_MODULES = "node_modules"; + protected static final String PACKAGE_JSON = "package.json"; + protected static final String DEV_DEPENDENCIES = "devDependencies"; + private final Logger logger = LoggerFactory.getLogger(YarnDependencyCollector.class); + private static final String YARN_COMMAND = isWindows() ? "yarn.cmd" : "yarn"; + private String fileSeparator = System.getProperty(Constants.FILE_SEPARATOR); + private static final String YARN_LOCK = "yarn.lock"; + + private Map devDependencies; + + + public YarnDependencyCollector(boolean includeDevDependencies, long npmTimeoutDependenciesCollector, boolean ignoreNpmLsErrors, boolean ignoreScripts) { + super(includeDevDependencies, npmTimeoutDependenciesCollector, ignoreNpmLsErrors, ignoreScripts); + } + + @Override + public Collection collectDependencies(String folder) { + if (!includeDevDependencies){ + // when 'indcludeDevDependenceis=false' - collecting the list of dev-dependencies so that later they're excluded from the list of dependencies + devDependencies = findDevDependencies(folder); + } + File yarnLock = new File(folder + fileSeparator + YARN_LOCK); + boolean yarnLockFound = yarnLock.isFile(); + Collection dependencies = new ArrayList<>(); + if (yarnLockFound){ + dependencies = parseYarnLock(yarnLock); + } else { + npmLsFailureStatus = true; + } + return getSingleProjectList(dependencies); + } + + protected String[] getInstallParams() { + return new String[]{YARN_COMMAND, Constants.INSTALL}; + } + + public boolean executePreparationStep(String folder) { + CommandLineProcess yarnInstallCommand = new CommandLineProcess(folder, getInstallParams()); + yarnInstallCommand.setTimeoutReadLineSeconds(this.npmTimeoutDependenciesCollector); + List linesOfYarnInstall; + try { + linesOfYarnInstall = yarnInstallCommand.executeProcess(); + if (yarnInstallCommand.isErrorInProcess()) { + for (String line : linesOfYarnInstall) { + if (line.startsWith(SUCCESS)) { + return true; + } + } + } + } catch (IOException e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } + return false; + } + + private List parseYarnLock(File yarnLock){ + List dependencyInfos = new ArrayList<>(); + HashMap parentsMap = new HashMap<>(); + HashMap childrenMap = new HashMap<>(); + FileReader fileReader = null; + try { + fileReader = new FileReader(yarnLock.getPath()); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String currLine; + boolean insideDependencies = false; + DependencyInfo dependencyInfo = null; + while ((currLine = bufferedReader.readLine()) != null){ + if (currLine.isEmpty() || currLine.startsWith(Constants.POUND) || currLine.trim().isEmpty()){ + insideDependencies = false; + continue; + } + logger.debug(currLine); + if (currLine.startsWith(Constants.WHITESPACE)) { + if (currLine.trim().startsWith(Constants.VERSION)){ + String version = currLine.substring(currLine.indexOf(Constants.QUOTATION_MARK) + 1, currLine.lastIndexOf(Constants.QUOTATION_MARK)); + dependencyInfo.setVersion(version); + dependencyInfo.setArtifactId(dependencyInfo.getGroupId() + Constants.DASH + version + TGZ); + } else if (currLine.trim().startsWith(RESOLVED)){ + String sha1 = currLine.substring(currLine.indexOf(Constants.POUND) + 1, currLine.lastIndexOf(Constants.QUOTATION_MARK)); + dependencyInfo.setSha1(sha1); + } else if (currLine.trim().startsWith(Constants.DEPENDENCIES) || currLine.trim().startsWith(OPTIONAL_DEPENDENCIES)) { + insideDependencies = true; + } else if (insideDependencies){ + String name = currLine.trim().replaceFirst(Constants.WHITESPACE, AT); + name = name.replaceAll(Constants.QUOTATION_MARK, Constants.EMPTY_STRING); + childrenMap.put(name, dependencyInfo); + } + } else { + String[] split = currLine.split(Constants.COMMA + Constants.WHITESPACE); + for (int i = 0; i < split.length; i++){ + String name = split[i].substring(0, split[i].length() - (split[i].endsWith(Constants.COLON) ? 1 : 0)); + name = name.replaceAll(Constants.QUOTATION_MARK,Constants.EMPTY_STRING); + String groupId = name.split(AT)[name.startsWith(AT) ? 1 : 0]; + if (i==0) { + dependencyInfo = new DependencyInfo(); + dependencyInfo.setGroupId(groupId); + dependencyInfo.setDependencyType(DependencyType.NPM); + String pathToPackageJson = yarnLock.getParent() + fileSeparator + NODE_MODULES + fileSeparator + groupId + fileSeparator + PACKAGE_JSON; + dependencyInfo.setSystemPath(pathToPackageJson); + dependencyInfo.setDependencyFile(pathToPackageJson); + dependencyInfo.setFilename(pathToPackageJson); + } + // adding the dependency to the parents map, if: it's not there already and either dev-dependencies should be included or + // they shouldn't but this is not a dev-dependency + if (parentsMap.get(name) == null && + ((includeDevDependencies || + (!includeDevDependencies && + (devDependencies.get(groupId) == null || devDependencies.get(groupId).equals(name.split(AT)[1]) == false))))){ + parentsMap.put(name, dependencyInfo); + } + } + } + } + for (String child : childrenMap.keySet()){ + if (parentsMap.get(child) != null && !isDescendant(parentsMap.get(child),childrenMap.get(child))) { + childrenMap.get(child).getChildren().add(parentsMap.get(child)); + } + } + for (String parent : parentsMap.keySet()){ + if (childrenMap.get(parent) == null){ + dependencyInfos.add(parentsMap.get(parent)); + } + } + } catch (Exception e){ + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } finally { + if (fileReader != null){ + try { + fileReader.close(); + } catch (IOException e) { + logger.error("can't close {}: {}", yarnLock.getPath(), e.getMessage()); + } + } + } + return dependencyInfos; + } + + // preventing circular dependencies by making sure the dependency is not a descendant of its own + private boolean isDescendant(DependencyInfo ancestor, DependencyInfo descendant){ + for (DependencyInfo child : ancestor.getChildren()){ + if (child.equals(descendant)){ + return true; + } + if (isDescendant(child, descendant)){ + return true; + } + } + return false; + } + + private Map findDevDependencies(String folder){ + Map devDependenciesMap = new HashMap<>(); + File packageJson = new File(folder + fileSeparator + PACKAGE_JSON); + if (packageJson.isFile()) { + try { + InputStream is = new FileInputStream(packageJson.getPath()); + String jsonText = IOUtils.toString(is, Constants.UTF8); + JSONObject json = new JSONObject(jsonText); + try { + devDependenciesMap = json.getJSONObject(DEV_DEPENDENCIES).toMap(); + } catch (JSONException e){ + logger.error("No '{}' node found in {}", DEV_DEPENDENCIES, packageJson); + } + + } catch (Exception e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + } + } + return devDependenciesMap; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetDependencyResolver.java new file mode 100644 index 0000000..3c24099 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetDependencyResolver.java @@ -0,0 +1,158 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.nuget; + +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.dependency.resolver.nuget.packagesConfig.NugetConfigFileType; +import org.whitesource.agent.dependency.resolver.nuget.packagesConfig.NugetPackagesConfigXmlParser; +import org.whitesource.fs.CommandLineArgs; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author yossi.weinberg + */ +public class NugetDependencyResolver extends AbstractDependencyResolver{ + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(NugetDependencyResolver.class); + public static final String CONFIG = ".config"; + public static final String CSPROJ = ".csproj"; + protected static final String CSHARP = ".cs"; + + /* --- Private Members --- */ + + private final String whitesourceConfiguration; + private final String bomPattern; + private final NugetConfigFileType nugetConfigFileType; + private boolean runPreStep; + private boolean ignoreSourceFiles; + + /* --- Constructor --- */ + + public NugetDependencyResolver(String whitesourceConfiguration, NugetConfigFileType nugetConfigFileType, boolean runPreStep,boolean ignoreSourceFiles) { + super(); + this.whitesourceConfiguration = whitesourceConfiguration; + this.nugetConfigFileType = nugetConfigFileType; + this.runPreStep = runPreStep; + this.ignoreSourceFiles=ignoreSourceFiles; + if (this.nugetConfigFileType == NugetConfigFileType.CONFIG_FILE_TYPE) { + bomPattern = Constants.PATTERN + CONFIG; + } else { + bomPattern = Constants.PATTERN + CSPROJ; + } + + + } + + /* --- Overridden methods --- */ + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set configFiles) { + if (this.nugetConfigFileType == NugetConfigFileType.CONFIG_FILE_TYPE && this.runPreStep) { + logger.debug("Trying to run pre step on packages.config files"); + NugetRestoreCollector nugetRestoreCollector = new NugetRestoreCollector(); + nugetRestoreCollector.executeRestore(projectFolder, configFiles); + Collection projects = nugetRestoreCollector.collectDependencies(projectFolder); + Collection dependencies = projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()); + return new ResolutionResult(dependencies, getExcludes(), getDependencyType(), topLevelFolder); + } else { + return getResolutionResultFromParsing(topLevelFolder, configFiles, false); + } + } + + protected ResolutionResult getResolutionResultFromParsing(String topLevelFolder, Set configFiles, boolean onlyDependenciesFromReferenceTag) { + Collection dependencies = parseNugetPackageFiles(configFiles, onlyDependenciesFromReferenceTag); + return new ResolutionResult(dependencies, getExcludes(), getDependencyType(), topLevelFolder); + } + + @Override + protected Collection getExcludes() { + List excludes = new LinkedList<>(); + excludes.add(CommandLineArgs.CONFIG_FILE_NAME); + if(ignoreSourceFiles) { + excludes.add(Constants.PATTERN + CSHARP); + } + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return new ArrayList<>(Arrays.asList(Constants.DLL, Constants.EXE, Constants.NUPKG, Constants.CS)); + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.NUGET; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.NUGET.name(); + } + + @Override + protected String[] getBomPattern() { + return new String[]{this.bomPattern}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(this.nugetConfigFileType == NugetConfigFileType.CONFIG_FILE_TYPE ? CONFIG : CSPROJ); + } + + @Override + protected Collection getLanguageExcludes() { + return new ArrayList<>(); + } + + protected Collection parseNugetPackageFiles(Set nugetDependencyFiles, boolean getDependenciesFromReferenceTag) { + // get configuration file path + Set dependencies = new HashSet<>(); + for (String nugetDependencyFile : nugetDependencyFiles) { + // don't scan the whitesource configuration file + // sometimes FSA is called from outside and there is no config file + if (whitesourceConfiguration == null || !new File(whitesourceConfiguration).getAbsolutePath().equals(nugetDependencyFile)) { + File configFile = new File(nugetDependencyFile); + // check filename again (just in case) + if (!configFile.getName().equals(CommandLineArgs.CONFIG_FILE_NAME)) { + NugetPackagesConfigXmlParser parser = new NugetPackagesConfigXmlParser(configFile, this.nugetConfigFileType); + Set dependenciesFromSingleFile = parser.parsePackagesConfigFile(getDependenciesFromReferenceTag, nugetDependencyFile); + if (!dependenciesFromSingleFile.isEmpty()) { + dependencies.addAll(dependenciesFromSingleFile); + } + } + } + } + return dependencies; + } + + @Override + protected Collection getRelevantScannedFolders(Collection scannedFolders) { + // Nuget resolver should scan all folders and should not remove any folder + return scannedFolders == null ? Collections.emptyList() : scannedFolders; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetRestoreCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetRestoreCollector.java new file mode 100644 index 0000000..3b773d6 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/NugetRestoreCollector.java @@ -0,0 +1,30 @@ +package org.whitesource.agent.dependency.resolver.nuget; + +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.dependency.resolver.dotNet.RestoreCollector; + +import java.nio.file.Paths; + +/** + * @author raz.nitzan + */ + +public class NugetRestoreCollector extends RestoreCollector { + + /* --- Statics Members --- */ + + private static final String NUGET_RESTORE_TMP_DIRECTORY = Paths.get(System.getProperty("java.io.tmpdir"), TempFolders.UNIQUE_DOTNET_TEMP_FOLDER).toString(); + private static final String NUGET_COMMAND = "nuget"; + private static final String PACKAGES_DIRECTORY = "-PackagesDirectory"; + + /* --- Constructors --- */ + + public NugetRestoreCollector() { + super(NUGET_RESTORE_TMP_DIRECTORY, NUGET_COMMAND); + } + + @Override + protected String[] getInstallParams(String pathToDownloadPackages, String csprojFile) { + return new String[]{NUGET_COMMAND, RESTORE, csprojFile, PACKAGES_DIRECTORY, pathToDownloadPackages}; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetConfigFileType.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetConfigFileType.java new file mode 100644 index 0000000..5d740db --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetConfigFileType.java @@ -0,0 +1,9 @@ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +/** + * @author raz.nitzan + */ +public enum NugetConfigFileType { + CONFIG_FILE_TYPE, + CSPROJ_TYPE +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojItemGroup.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojItemGroup.java new file mode 100644 index 0000000..0d8f42f --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojItemGroup.java @@ -0,0 +1,49 @@ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author raz.nitzan + */ +@Root(name="ItemGroup", strict=false) +public class NugetCsprojItemGroup { + + /* --- Members --- */ + + @ElementList(inline=true, required=false) + private List packagesReference = new LinkedList<>(); + + @ElementList(inline=true, required=false) + private List references = new LinkedList<>(); + + /* --- Constructors --- */ + + public NugetCsprojItemGroup(List packagesReference, List references) { + this.packagesReference = packagesReference; + this.references = references; + } + + public NugetCsprojItemGroup() { + } + + /* --- Getters / Setters --- */ + public List getPackageReference() { + return packagesReference; + } + + public void setPackageReference(List packagesReference) { + this.packagesReference = packagesReference; + } + + public List getReferences() { + return this.references; + } + + public void setReferences(List references) { + this.references = references; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojPackages.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojPackages.java new file mode 100644 index 0000000..2241926 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetCsprojPackages.java @@ -0,0 +1,28 @@ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author raz.nitzan + */ +@Root(name="Project", strict=false) +public class NugetCsprojPackages { + /* --- Members --- */ + + @ElementList(inline=true, required=false) + private List nugetItemGroups = new LinkedList<>(); + + /* --- Getters / Setters --- */ + + public List getNugetItemGroups() { + return nugetItemGroups; + } + + public void setNugetItemGroups(List nugetItemGroups) { + this.nugetItemGroups = (List) nugetItemGroups; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackage.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackage.java new file mode 100644 index 0000000..d413b1f --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackage.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +/** + * @author yossi.weinberg + */ +@Root(name="package", strict=false) +public class NugetPackage implements NugetPackageInterface { + + /* --- Members --- */ + + @Attribute(name="id", required=false) + private String pkgName; + @Attribute(name="version", required=false) + private String pkgVersion; + + /* --- Constructors --- */ + + public NugetPackage(String pkgName, String pkgVersion) { + this.pkgName = pkgName; + this.pkgVersion = pkgVersion; + } + + public NugetPackage() { + } + + /* --- Getters / Setters --- */ + + public String getPkgName() { + return pkgName; + } + + public void setPkgName(String pkgName) { + this.pkgName = pkgName; + } + + public String getPkgVersion() { + return pkgVersion; + } + + public void setPkgVersion(String pkgVersion) { + this.pkgVersion = pkgVersion; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackageInterface.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackageInterface.java new file mode 100644 index 0000000..f337c83 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackageInterface.java @@ -0,0 +1,15 @@ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + + +import javax.xml.bind.annotation.XmlTransient; + +/** + * @author raz.nitzan + */ +@XmlTransient +public interface NugetPackageInterface { + String getPkgName(); + void setPkgName(String pkgName); + String getPkgVersion(); + void setPkgVersion(String pkgVersion); +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackages.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackages.java new file mode 100644 index 0000000..a9fdf61 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackages.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author yossi.weinberg + */ +@Root(name="packages", strict=false) +public class NugetPackages { + + /* --- Members --- */ + + @ElementList(inline=true, required=false) + private List nugetPackages = new LinkedList<>(); + + /* --- Getters / Setters --- */ + + public List getNugetPackages() { + return nugetPackages; + } + + public void setNugetPackages(List nugetPackages) { + this.nugetPackages = (List) nugetPackages; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackagesConfigXmlParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackagesConfigXmlParser.java new file mode 100644 index 0000000..dd70ef8 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/NugetPackagesConfigXmlParser.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +import org.apache.commons.lang.StringUtils; +import org.simpleframework.xml.core.Persister; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.DependencyInfoFactory; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; + +import java.io.File; +import java.io.Serializable; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * @author yossi.weinberg + */ +public class NugetPackagesConfigXmlParser implements Serializable { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(NugetPackagesConfigXmlParser.class); + + /* --- Members --- */ + + private File xml; + + private NugetConfigFileType nugetConfigFileType; + + /* --- Constructors --- */ + + public NugetPackagesConfigXmlParser(File xml, NugetConfigFileType nugetConfigFileType) { + this.xml = xml; + this.nugetConfigFileType = nugetConfigFileType; + } + + /* --- Public methods --- */ + + /** + * Parse packages.config or csproj file + * + * @param getDependenciesFromReferenceTag - flag to indicate weather to get dependencies form reference tag or not + * @return Set of DependencyInfos + */ + public Set parsePackagesConfigFile(boolean getDependenciesFromReferenceTag, String nugetDependencyFile) { + Persister persister = new Persister(); + Set dependencies = new HashSet<>(); + try { + // case of packages.config file + if (this.nugetConfigFileType == NugetConfigFileType.CONFIG_FILE_TYPE) { + NugetPackages packages = persister.read(NugetPackages.class, xml); + if (!getDependenciesFromReferenceTag) { + dependencies.addAll(collectDependenciesFromNugetConfig(packages, nugetDependencyFile)); + } + // case of csproj file + } else { + NugetCsprojPackages csprojPackages = persister.read(NugetCsprojPackages.class, xml); + NugetPackages packages = getNugetPackagesFromCsproj(csprojPackages); + if (!getDependenciesFromReferenceTag) { + dependencies.addAll(collectDependenciesFromNugetConfig(packages, nugetDependencyFile)); + } + dependencies.addAll(getDependenciesFromReferencesTag(csprojPackages)); + } + dependencies.stream().forEach(dependencyInfo -> dependencyInfo.setSystemPath(this.xml.getPath())); + } catch (Exception e) { + logger.warn("Unable to parse suspected Nuget package configuration file {}", xml, e.getMessage()); + } + return dependencies; + } + + private NugetPackages getNugetPackagesFromCsproj(NugetCsprojPackages csprojPackages) { + List nugetPackages = new LinkedList<>(); + for (NugetCsprojItemGroup csprojPackage : csprojPackages.getNugetItemGroups()) { + for (PackageReference packageReference : csprojPackage.getPackageReference()) { + if (packageReference != null && packageReference.getPkgName() != null && packageReference.getPkgVersion() != null) { + nugetPackages.add(new NugetPackage(packageReference.getPkgName(), packageReference.getPkgVersion())); + } + } + } + NugetPackages nugetPackagesResult = new NugetPackages(); + nugetPackagesResult.setNugetPackages(nugetPackages); + return nugetPackagesResult; + } + + private Set getDependenciesFromReferencesTag(NugetCsprojPackages csprojPackages) { + Set dependencies = new HashSet<>(); + DependencyInfoFactory dependencyInfoFactory = new DependencyInfoFactory(); + for (NugetCsprojItemGroup csprojPackage : csprojPackages.getNugetItemGroups()) { + for (ReferenceTag referenceTag : csprojPackage.getReferences()) { + // Ignore the dependency if the hint path is blank + if (StringUtils.isNotEmpty(referenceTag.getHintPath())) { + Path basePath = FileSystems.getDefault().getPath(this.xml.getPath()); + Path hintParentResolvedPath = basePath.getParent().resolve(referenceTag.getHintPath()); + String hintAbsolutePath = hintParentResolvedPath.normalize().toAbsolutePath().toString(); + File fileFromHintPath = new File(hintAbsolutePath); + DependencyInfo dependency = dependencyInfoFactory.createDependencyInfo(fileFromHintPath.getParentFile(), fileFromHintPath.getName()); + if (dependency != null) { + if (StringUtils.isNotEmpty(referenceTag.getVersion())) { + dependency.setVersion(referenceTag.getVersion()); + } + dependencies.add(dependency); + } + } + } + } + return dependencies; + } + + private Set collectDependenciesFromNugetConfig(NugetPackages configNugetPackage, String nugetDependencyFile) { + Set dependencies = new HashSet<>(); + List nugetPackages = configNugetPackage.getNugetPackages(); + if (nugetPackages != null) { + for (NugetPackage nugetPackage : nugetPackages) { + if (StringUtils.isNotBlank(nugetPackage.getPkgName()) && StringUtils.isNotBlank(nugetPackage.getPkgVersion())) { + DependencyInfo dependency = new DependencyInfo(); + dependency.setGroupId(nugetPackage.getPkgName()); + dependency.setArtifactId(nugetPackage.getPkgName()); + dependency.setVersion(nugetPackage.getPkgVersion()); + dependency.setDependencyType(DependencyType.NUGET); + dependency.setDependencyFile(nugetDependencyFile); + dependency.setSystemPath(nugetDependencyFile); + dependencies.add(dependency); + } + } + } + return dependencies; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/PackageReference.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/PackageReference.java new file mode 100644 index 0000000..68f8f11 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/PackageReference.java @@ -0,0 +1,46 @@ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +/** + * @author raz.nitzan + */ +@Root(name="PackageReference", strict=false) +public class PackageReference implements NugetPackageInterface { + + /* --- Members --- */ + + @Attribute(name="Include", required=false) + private String pkgName; + @Attribute(name="Version", required=false) + private String pkgVersion; + + /* --- Constructors --- */ + + public PackageReference(String pkgName, String pkgVersion) { + this.pkgName = pkgName; + this.pkgVersion = pkgVersion; + } + + public PackageReference() { + } + + /* --- Getters / Setters --- */ + + public String getPkgName() { + return pkgName; + } + + public void setPkgName(String pkgName) { + this.pkgName = pkgName; + } + + public String getPkgVersion() { + return pkgVersion; + } + + public void setPkgVersion(String pkgVersion) { + this.pkgVersion = pkgVersion; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/ReferenceTag.java b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/ReferenceTag.java new file mode 100644 index 0000000..1db4355 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/nuget/packagesConfig/ReferenceTag.java @@ -0,0 +1,60 @@ +package org.whitesource.agent.dependency.resolver.nuget.packagesConfig; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** + * @author raz.nitzan + */ +@Root(name="Reference", strict=false) +public class ReferenceTag { + + /* --- Members --- */ + + @Attribute(name="Include", required=false) + private String name; + + @Attribute(name="Version", required=false) + private String version; + + @Element(name="HintPath", required=false) + private String hintPath; + + /* --- Constructors --- */ + + public ReferenceTag(String name, String version, String hintPath) { + this.name = name; + this.version = version; + this.hintPath = hintPath; + } + + public ReferenceTag() { + } + + /* --- Getters / Setters --- */ + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String pkgVersion) { + this.version = pkgVersion; + } + + public String getHintPath() { + return this.hintPath; + } + + public void setHintPath(String hintPath) { + this.hintPath = hintPath; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/packageManger/LinuxPkgManagerCommand.java b/src/main/java/org/whitesource/agent/dependency/resolver/packageManger/LinuxPkgManagerCommand.java new file mode 100644 index 0000000..bbe6eaf --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/packageManger/LinuxPkgManagerCommand.java @@ -0,0 +1,19 @@ +package org.whitesource.agent.dependency.resolver.packageManger; + +public enum LinuxPkgManagerCommand { + + DEBIAN("dpkg -l"), + RPM("rpm -qa"), + ALPINE("apk -vv info"), + ARCH_LINUX("pacman -Q"); + + private String command; + + LinuxPkgManagerCommand(String url) { + this.command = url; + } + + public String getCommand() { + return command; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/packageManger/PackageManagerExtractor.java b/src/main/java/org/whitesource/agent/dependency/resolver/packageManger/PackageManagerExtractor.java new file mode 100644 index 0000000..0f04f79 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/packageManger/PackageManagerExtractor.java @@ -0,0 +1,215 @@ +package org.whitesource.agent.dependency.resolver.packageManger; + +import com.aragost.javahg.log.Logger; +import com.aragost.javahg.log.LoggerFactory; +import com.google.common.io.ByteStreams; +import org.apache.commons.lang.StringUtils; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.MessageFormat; +import java.util.*; + +/** + * Created by anna.rozin + */ +public class PackageManagerExtractor { + + /* --- Statics Members --- */ + + private final Logger logger = LoggerFactory.getLogger(PackageManagerExtractor.class); + + private static final int DEBIAN_PACKAGE_NAME_INDEX = 0; + private static final int DEBIAN_PACKAGE_VERSION_INDEX = 1; + private static final int DEBIAN_PACKAGE_ARCH_INDEX = 2; + private static final String DEBIAN_INSTALLED_PACKAGE_PREFIX = "ii"; + private static final String DEBIAN_PACKAGE_PATTERN = "{0}_{1}_{2}.deb"; + private static final String RPM_PACKAGE_PATTERN = "{0}.rpm"; + private static final String ALPINE_PACKAGE_PATTERN = "{0}.apk"; + private static final String ALPINE_PACKAGE_SPLIT_PATTERN = " - "; + private static final String ARCH_LINUX_PACKAGE_PATTERN = "{0}-{1}-{2}.pkg.tar.xz"; + private static final List SYSTEM_ARCHITECTURES = Arrays.asList("x86_64", "i686", "any"); + private static final String ARCH_LINUX_PACKAGE_SPLIT_PATTERN = " "; + private static final String NEW_LINE = "\\r?\\n"; + private static final String NON_ASCII_CHARS = "[^\\x20-\\x7e]"; + private static final String ARCH_LINUX_ARCHITECTURE_COMMAND = "uname -m"; + + /* --- Constructors --- */ + + public PackageManagerExtractor() { + } + + /* --- Public methods --- */ + + public Collection createProjects() { + List packages = new LinkedList<>(); + Collection projectInfos = new LinkedList<>(); + InputStream inputStream = null; + byte[] bytes = null; + Process process = null; + logger.info("File System Agent is resolving package manger dependencies only"); + //For each flavor command check installed packages + for (LinuxPkgManagerCommand linuxPkgManagerCommand : LinuxPkgManagerCommand.values()) { + try { + logger.debug("Trying to run command {}", linuxPkgManagerCommand.getCommand()); + process = Runtime.getRuntime().exec(linuxPkgManagerCommand.getCommand()); + inputStream = process.getInputStream(); + if (inputStream.read() == -1) { + logger.error("Unable to execute - {} , unix flavor does not support this command ", linuxPkgManagerCommand.getCommand()); + } else { + bytes = ByteStreams.toByteArray(inputStream); + //Get the installed packages (name,version,architecture) from inputStream + logger.info("Succeed to run the command - {} ", linuxPkgManagerCommand.getCommand()); + switch (linuxPkgManagerCommand) { + case DEBIAN: + logger.debug("Getting Debian installed Packages"); + createDebianProject(bytes, packages); + break; + case RPM: + logger.debug("Getting RPM installed Packages"); + createRpmProject(bytes, packages); + break; + case ARCH_LINUX: + logger.debug("Getting Arch Linux installed Packages"); + createArchLinuxProject(bytes, packages); + break; + case ALPINE: + logger.debug("Getting Alpine installed Packages"); + createAlpineProject(bytes, packages); + break; + default: + break; + } + } + // Create new AgentProjectInfo object and add it into a list of AgentProjectInfo + if (packages.size() > 0) { + logger.debug("Creating new AgentProjectInfo object"); + AgentProjectInfo projectInfo = new AgentProjectInfo(); + projectInfo.setDependencies(packages); + projectInfos.add(projectInfo); + packages = new LinkedList<>(); + } else { + logger.info("Couldn't find unix package manager dependencies"); + } + } catch (IOException e) { + logger.warn("Couldn't resolve : {}", linuxPkgManagerCommand.name()); + } + } + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + logger.error("InputStream exception : {}", e.getMessage()); + } + return projectInfos; + } + + public void createDebianProject(byte[] bytes, List packages) { + logger.info("Trying to resolve debian packages"); + String linesStr = new String(bytes); + String[] lines = linesStr.split(NEW_LINE); + for (String line : lines) { + line = line.replaceAll(NON_ASCII_CHARS, Constants.EMPTY_STRING); + if (line.startsWith(DEBIAN_INSTALLED_PACKAGE_PREFIX)) { + List args = new ArrayList<>(); + for (String s : line.split(Constants.WHITESPACE)) { + if (StringUtils.isNotBlank(s) && !s.equals(DEBIAN_INSTALLED_PACKAGE_PREFIX)) { + args.add(s); + } + } + if (args.size() >= 3) { + // names may contain the arch (i.e. package_name:amd64) - remove it + String name = args.get(DEBIAN_PACKAGE_NAME_INDEX); + if (name.contains(Constants.COLON)) { + name = name.substring(0, name.indexOf(Constants.COLON)); + } + // versions may contain a + String version = args.get(DEBIAN_PACKAGE_VERSION_INDEX); + if (version.contains(Constants.COLON)) { + version = version.substring(version.indexOf(Constants.COLON) + 1); + } + String arch = args.get(DEBIAN_PACKAGE_ARCH_INDEX); + packages.add(new DependencyInfo( + null, MessageFormat.format(DEBIAN_PACKAGE_PATTERN, name, version, arch), version)); + } + } + } + } + + public void createRpmProject(byte[] bytes, List packages) { + logger.info("Trying to resolve RPM packages"); + String linesStr = new String(bytes); + String[] lines = linesStr.split(NEW_LINE); + for (String line : lines) { + if (StringUtils.isNotBlank(line)) { + packages.add(new DependencyInfo(null, MessageFormat.format(RPM_PACKAGE_PATTERN, line), null)); + } + } + } + + public void createArchLinuxProject(byte[] bytes, List packages) { + logger.info("Trying to resolve Arch Linux packages"); + String linesStr = new String(bytes); + String[] lines = linesStr.split(NEW_LINE); + String arch = getSystemArchitecture(); + if (StringUtils.isNotBlank(arch)) { + for (String line : lines) { + line = line.replaceAll(NON_ASCII_CHARS, Constants.EMPTY_STRING); + String[] split = line.split(ARCH_LINUX_PACKAGE_SPLIT_PATTERN); + logger.info(split[0]); + if (split.length == 2) { + packages.add(new DependencyInfo(null, MessageFormat.format(ARCH_LINUX_PACKAGE_PATTERN, + split[0], split[1], arch), null)); + + } + } + } + } + + public void createAlpineProject(byte[] bytes, List packages) { + logger.info("Trying to resolve Alpine packages"); + String linesStr = new String(bytes); + String[] lines = linesStr.split(NEW_LINE); + for (String line : lines) { + line = line.replaceAll(NON_ASCII_CHARS, Constants.EMPTY_STRING); + if (line.contains(ALPINE_PACKAGE_SPLIT_PATTERN)) { + String[] split = line.split(ALPINE_PACKAGE_SPLIT_PATTERN); + if (split.length > 0) { + packages.add(new DependencyInfo(null, MessageFormat.format(ALPINE_PACKAGE_PATTERN, split[0]), null)); + } + } + } + } + + + /* --- Private methods --- */ + + private String getSystemArchitecture() { + String arch = Constants.EMPTY_STRING; + String outputStr = null; + BufferedReader bufferedReader = null; + Process process = null; + try { + process = Runtime.getRuntime().exec(ARCH_LINUX_ARCHITECTURE_COMMAND); + process.waitFor(); + bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + outputStr = bufferedReader.readLine(); + if (StringUtils.isNotBlank(outputStr) && SYSTEM_ARCHITECTURES.contains(outputStr)) { + arch = outputStr; + } + bufferedReader.close(); + } catch (IOException e) { + logger.warn("Error processing arch linux command {}, error : {}", LinuxPkgManagerCommand.ARCH_LINUX, e.getMessage()); + } catch (InterruptedException e) { + logger.warn("Error InterruptedException {}, error : {}", LinuxPkgManagerCommand.ARCH_LINUX, e.getMessage()); + } + return arch; + } + +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/php/PhpDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/php/PhpDependencyResolver.java new file mode 100644 index 0000000..f6cb094 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/php/PhpDependencyResolver.java @@ -0,0 +1,298 @@ +package org.whitesource.agent.dependency.resolver.php; + +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.hash.HashCalculator; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.dependency.resolver.php.phpModel.PhpModel; +import org.whitesource.agent.dependency.resolver.php.phpModel.PhpPackage; +import org.whitesource.agent.utils.CommandLineProcess; + +import java.io.*; +import java.util.*; + +import static org.whitesource.agent.Constants.FORWARD_SLASH; +import static org.whitesource.agent.Constants.INSTALL; +import static org.whitesource.agent.Constants.PATTERN; + +/** + * @author chen.luigi + */ +public class PhpDependencyResolver extends AbstractDependencyResolver { + + /* --- Static members --- */ + + private static final String COMPOSER_LOCK = "composer.lock"; + private static final String COMPOSER_JSON = "composer.json"; + private static final String PHP_EXTENSION = ".php"; + private static final String COMPOSER_BAT = "composer.bat"; + private static final String COMPOSER = "composer"; + private static final String PHP_INCLUDE_NO_DEV = "--no-dev"; + private static final String REQUIRE = "require"; + private static final String REQUIRE_DEV = "require-dev"; + private static final String PHP = "php"; + private static final List PHP_PATTERN_EXTENSION = Arrays.asList(PATTERN + ".php"); + + /* --- Private Members --- */ + + private final Logger logger = LoggerFactory.getLogger(PhpDependencyResolver.class); + + private boolean phpPreStep; + private boolean includeDevDependencies; + private HashCalculator hashCalculator = new HashCalculator(); + private boolean addSha1; + + /* --- Constructors --- */ + + public PhpDependencyResolver(boolean phpPreStep, boolean includeDevDependencies, boolean addSha1) { + super(); + this.phpPreStep = phpPreStep; + this.includeDevDependencies = includeDevDependencies; + this.addSha1 = addSha1; + } + + /* --- Overridden methods --- */ + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + boolean installSuccess = true; + Collection dependencyInfos = new LinkedList<>(); + Collection directDependencies = new LinkedList<>(); + File composerLock = new File(topLevelFolder + FORWARD_SLASH + COMPOSER_LOCK); + + // run pre step according to phpPreStep flag + if (phpPreStep) { + installSuccess = !executePreStepCommand(topLevelFolder); + } else { + if (!composerLock.exists()) { + logger.warn("Could not find {} file in {}. Please execute {} {} first.", COMPOSER_LOCK, topLevelFolder, COMPOSER, INSTALL); + } + } + if (installSuccess && composerLock.exists()) { + try { + Map requireMap = new HashMap<>(); + InputStream is = new FileInputStream(topLevelFolder + FORWARD_SLASH + COMPOSER_JSON); + String jsonText = IOUtils.toString(is); + JSONObject json = new JSONObject(jsonText); + if (json.has(REQUIRE)) { + JSONObject require = json.getJSONObject(REQUIRE); + requireMap = require.toMap(); + } + if (includeDevDependencies) { + if (json.has(REQUIRE_DEV)) { + JSONObject requireDev = json.getJSONObject(REQUIRE_DEV); + Map requireDevMap = requireDev.toMap(); + requireMap.putAll(requireDevMap); + } + } + if (!requireMap.isEmpty()) { + if (requireMap.containsKey(PHP)) { + requireMap.remove(PHP); + } + directDependencies.addAll(requireMap.keySet()); + } + + } catch (IOException e) { + logger.error("Didn't succeed to read {} - {} ", COMPOSER_JSON, e.getMessage()); + } + } + + if (!directDependencies.isEmpty()) { + try { + JsonReader jsonReader = new JsonReader(new FileReader(topLevelFolder + FORWARD_SLASH + COMPOSER_LOCK)); + PhpModel phpModel = new Gson().fromJson(jsonReader, PhpModel.class); + Collection phpPackages = phpModel.getPhpPackages(); + if (includeDevDependencies) { + phpPackages.addAll(phpModel.getPhpPackagesDev()); + } + if (!phpPackages.isEmpty()) { + dependencyInfos = createDependencyInfos(phpPackages, dependencyInfos, directDependencies); + } else { + logger.debug("The file {} is empty", COMPOSER_LOCK); + } + } catch (IOException e) { + logger.error(e.getMessage()); + } + } + return new ResolutionResult(dependencyInfos, getExcludes(), getDependencyType(), topLevelFolder); + } + + @Override + protected Collection getExcludes() { + return PHP_PATTERN_EXTENSION; + } + + @Override + public Collection getSourceFileExtensions() { + return new ArrayList<>(Arrays.asList(PHP_EXTENSION, COMPOSER_JSON)); + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.PHP; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.PHP.name(); + } + + @Override + public String[] getBomPattern() { + return new String[]{Constants.PATTERN + COMPOSER_JSON}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(COMPOSER_JSON); + } + + @Override + protected Collection getLanguageExcludes() { + return new LinkedList<>(); + } + + /* --- Private methods --- */ + + // create dependencyInfo objects from each direct dependency + private Collection createDependencyInfos(Collection phpPackages, Collection dependencyInfos, Collection directDependencies) { + HashMap> parentToChildMap = new HashMap<>(); + HashMap packageDependencyMap = new HashMap<>(); + // collect packages data and create its dependencyInfo + for (PhpPackage phpPackage : phpPackages) { + DependencyInfo dependencyInfo = createDependencyInfo(phpPackage); + if (dependencyInfo != null) { + parentToChildMap.put(dependencyInfo, phpPackage.getPackageRequire().keySet()); + packageDependencyMap.put(phpPackage.getName(), dependencyInfo); + } else { + logger.debug("Didn't succeed to create dependencyInfo for {}", phpPackage.getName()); + } + } + if (!packageDependencyMap.isEmpty()) { + for (String directDependency : directDependencies) { + // create hierarchy tree + DependencyInfo dependencyInfo = packageDependencyMap.get(directDependency); + if (dependencyInfo != null) { + collectChildren(dependencyInfo, packageDependencyMap, parentToChildMap); + dependencyInfos.add(dependencyInfo); + } else { + logger.debug("Didn't found {} in map {}", directDependency, packageDependencyMap.getClass().getName()); + } + } + } else { + logger.debug("The map {} is empty ", packageDependencyMap.getClass().getName()); + } + return dependencyInfos; + } + + // convert phpPackage to dependencyInfo object + private DependencyInfo createDependencyInfo(PhpPackage phpPackage) { + String groupId = getGroupIdFromName(phpPackage); + String artifactId = phpPackage.getName(); + String version = phpPackage.getVersion(); + String commit = phpPackage.getPackageSource().getReference(); + if (StringUtils.isNotBlank(version) || StringUtils.isNotBlank(commit)) { + DependencyInfo dependencyInfo = new DependencyInfo(groupId, artifactId, version); + dependencyInfo.setCommit(commit); + dependencyInfo.setDependencyType(getDependencyType()); + if (this.addSha1) { + String sha1 = null; + String sha1Source = StringUtils.isNotBlank(version) ? version : commit; + try { + sha1 = this.hashCalculator.calculateSha1ByNameVersionAndType(artifactId, sha1Source, DependencyType.PHP); + } catch (IOException e) { + logger.debug("Failed to calculate sha1 of: {}", artifactId); + } + if (sha1 != null) { + dependencyInfo.setSha1(sha1); + } + } + return dependencyInfo; + } else { + logger.debug("The parameters version and commit of {} are null", phpPackage.getName()); + return null; + } + } + + // collect children's recursively for each dependencyInfo object + private void collectChildren(DependencyInfo dependencyInfo, HashMap packageDependencyMap, + HashMap> requireDependenciesMap) { + Collection requires = requireDependenciesMap.get(dependencyInfo); + // check if dependencyInfo object already have children's + if (dependencyInfo.getChildren().isEmpty()) { + for (String require : requires) { + DependencyInfo dependencyChild = packageDependencyMap.get(require); + if (dependencyChild != null) { + dependencyInfo.getChildren().add(dependencyChild); + collectChildren(dependencyChild, packageDependencyMap, requireDependenciesMap); + } + } + } + } + + // get the groupId from the name of package + private String getGroupIdFromName(PhpPackage phpPackage) { + String groupId = null; + if (StringUtils.isNotBlank(phpPackage.getName())) { + String packageName = phpPackage.getName(); + String[] gavCoordinates = packageName.split(FORWARD_SLASH); + groupId = gavCoordinates[0]; + } + return groupId; + } + + // get the artifactId from the name of package + private String getArtifactIdFromName(PhpPackage phpPackage) { + String artifactId = null; + if (StringUtils.isNotBlank(phpPackage.getName())) { + String packageName = phpPackage.getName(); + String[] gavCoordinates = packageName.split(FORWARD_SLASH); + artifactId = gavCoordinates[1]; + } + return artifactId; + } + + // execute pre step command (composer install) + private boolean executePreStepCommand(String topLevelFolder) { + String[] command; + if (DependencyCollector.isWindows()) { + command = getCommand(COMPOSER_BAT); + } else { + command = getCommand(COMPOSER); + } + String commandString = String.join(Constants.WHITESPACE, command); + File file = new File(topLevelFolder + FORWARD_SLASH + COMPOSER_JSON); + CommandLineProcess composerInstall = null; + if (file.exists()) { + logger.info("Running install command : {}", commandString); + composerInstall = new CommandLineProcess(topLevelFolder, command); + } + try { + composerInstall.executeProcessWithoutOutput(); + } catch (IOException e) { + logger.warn("Could not run {} in folder {} : {}", commandString, topLevelFolder, e.getMessage()); + return true; + } + return composerInstall.isErrorInProcess(); + } + + private String[] getCommand(String firstCommand) { + String[] command; + if (includeDevDependencies) { + command = new String[]{firstCommand, INSTALL}; + } else { + command = new String[]{firstCommand, INSTALL, PHP_INCLUDE_NO_DEV}; + } + return command; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PackageSource.java b/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PackageSource.java new file mode 100644 index 0000000..7977969 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PackageSource.java @@ -0,0 +1,58 @@ +package org.whitesource.agent.dependency.resolver.php.phpModel; + +import java.io.Serializable; +import java.util.Objects; + +/** + * @author chen.luigi + */ +public class PackageSource implements Serializable { + + /* --- Static members --- */ + + private static final long serialVersionUID = 7066960176089576432L; + + private String reference; + + /* --- Constructors --- */ + + public PackageSource() { + } + + public PackageSource(String reference) { + this.reference = reference; + } + + /* --- Overridden methods --- */ + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PackageSource)) return false; + PackageSource that = (PackageSource) o; + return Objects.equals(reference, that.reference); + } + + @Override + public int hashCode() { + + return Objects.hash(reference); + } + + @Override + public String toString() { + return "PackageSource{" + + "reference='" + reference + '\'' + + '}'; + } + + /* --- Getters / Setters --- */ + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpModel.java b/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpModel.java new file mode 100644 index 0000000..54051d0 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpModel.java @@ -0,0 +1,70 @@ +package org.whitesource.agent.dependency.resolver.php.phpModel; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Objects; + +/** + * @author chen.luigi + */ +public class PhpModel implements Serializable { + + /* --- Private Members --- */ + + private static final long serialVersionUID = 3790948889265089807L; + + @SerializedName("packages") + private Collection phpPackages; + + @SerializedName("packages-dev") + private Collection phpPackagesDev; + + /* --- Constructors --- */ + + public PhpModel() { + phpPackages = new LinkedList<>(); + phpPackagesDev = new LinkedList<>(); + } + + public PhpModel(Collection phpPackages, Collection phpPackagesDev) { + this.phpPackages = phpPackages; + this.phpPackagesDev = phpPackagesDev; + } + + /* --- Overridden methods --- */ + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PhpModel)) return false; + PhpModel phpModel = (PhpModel) o; + return Objects.equals(phpPackages, phpModel.phpPackages) && + Objects.equals(phpPackagesDev, phpModel.phpPackagesDev); + } + + @Override + public int hashCode() { + return Objects.hash(phpPackages, phpPackagesDev); + } + + /* --- Getters / Setters --- */ + + public Collection getPhpPackages() { + return phpPackages; + } + + public void setPhpPackages(Collection phpPackages) { + this.phpPackages = phpPackages; + } + + public Collection getPhpPackagesDev() { + return phpPackagesDev; + } + + public void setPhpPackagesDev(Collection phpPackagesDev) { + this.phpPackagesDev = phpPackagesDev; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpPackage.java b/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpPackage.java new file mode 100644 index 0000000..f08f216 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/php/phpModel/PhpPackage.java @@ -0,0 +1,94 @@ +package org.whitesource.agent.dependency.resolver.php.phpModel; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +/** + * @author chen.luigi + */ +public class PhpPackage implements Serializable { + + /* --- Static members --- */ + + private static final long serialVersionUID = -1797919662008607242L; + + /* --- Private Members --- */ + + private String name; + private String version; + + @SerializedName("source") + private PackageSource packageSource; + + @SerializedName("require") + private HashMap packageRequire; + + /* --- Constructors --- */ + + public PhpPackage() { + packageRequire = new HashMap<>(); + } + + public PhpPackage(String name, String version, PackageSource packageSource, HashMap packageRequire) { + this.name = name; + this.version = version; + this.packageSource = packageSource; + this.packageRequire = packageRequire; + } + + /* --- Overridden methods --- */ + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PhpPackage)) return false; + PhpPackage that = (PhpPackage) o; + return Objects.equals(name, that.name) && + Objects.equals(version, that.version) && + Objects.equals(packageSource, that.packageSource) && + Objects.equals(packageRequire, that.packageRequire); + } + + @Override + public int hashCode() { + + return Objects.hash(name, version, packageSource, packageRequire); + } + + /* --- Getters / Setters --- */ + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public PackageSource getPackageSource() { + return packageSource; + } + + public void setPackageSource(PackageSource packageSource) { + this.packageSource = packageSource; + } + + public HashMap getPackageRequire() { + return packageRequire; + } + + public void setPackageRequire(HashMap packageRequire) { + this.packageRequire = packageRequire; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/python/DependenciesFileType.java b/src/main/java/org/whitesource/agent/dependency/resolver/python/DependenciesFileType.java new file mode 100644 index 0000000..b6767a6 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/python/DependenciesFileType.java @@ -0,0 +1,10 @@ +package org.whitesource.agent.dependency.resolver.python; + +/** + * @author raz.nitzan + */ +public enum DependenciesFileType { + REQUIREMENTS_TXT, + SETUP_PY, + PIPFILE +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyCollector.java b/src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyCollector.java new file mode 100644 index 0000000..457b3b5 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyCollector.java @@ -0,0 +1,702 @@ +package org.whitesource.agent.dependency.resolver.python; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.hash.ChecksumUtils; +import org.whitesource.agent.utils.CommandLineProcess; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * @author raz.nitzan + */ +public class PythonDependencyCollector extends DependencyCollector { + /* -- Members -- */ + + private boolean installVirtualEnv; + private boolean resolveHierarchyTree; + private boolean ignorePipInstallErrors; + private boolean ignorePipEnvInstallErrors; + private boolean runPipEnvPreStep; + private boolean pipenvInstallDevDependencies; + private String requirementsTxtOrSetupPyPath; + private String pythonPath; + private String x; + private String tempDirPackages; + private String tempDirVirtualenv; + private String topLevelFolder; + private AtomicInteger counterFolders = new AtomicInteger(0); + private DependenciesFileType dependencyFileType; + private String tempDirDirectPackages; + + + private final Logger logger = LoggerFactory.getLogger(org.whitesource.agent.dependency.resolver.python.PythonDependencyResolver.class); + + /* --- Static members --- */ + + private static final String VIRTUALENV = "virtualenv"; + private static final String USER = "--user"; + private static final String SOURCE = "source"; + private static final String BIN_ACTIVATE = "/env/bin/activate"; + private static final String SCRIPTS_ACTIVATE = "\\env\\Scripts\\activate.bat"; + private static final String AND = "&&"; + private static final String PIPDEPTREE = "pipdeptree"; + private static final String M = "-m"; + private static final String ENV = "/env"; + private static final String JSON_TREE = "--json-tree"; + private static final String HIERARCHY_TREE_TXT = "/HierarchyTree.txt"; + private static final String PACKAGE_NAME = "package_name"; + private static final String INSTALLED_VERSION = "installed_version"; + private static final String DEPENDENCIES = "dependencies"; + private static final String LOWER_COMMA = "_"; + private static final String F = "-f"; + private static final String COMMA = "-"; + private static final String EMPTY_STRING = ""; + private static final String DOWNLOAD = "download"; + private static final String INSTALL = "install"; + private static final String R_PARAMETER = "-r"; + private static final String D_PARAMETER = "-d"; + private static final String COMMENT_SIGN_PYTHON = "#"; + private static final int NUM_THREADS = 8; + private static final String FORWARD_SLASH = "/"; + private static final String SCRIPT_SH = "/script.sh"; + private static final String BIN_BASH = "#!/bin/bash"; + private static final String ARROW = ">"; + private static final String NO_DEPS = "--no-deps"; + private static final String[] DEFAULT_PACKAGES_IN_PIPDEPTREE = new String[] {PIPDEPTREE, "setuptools", "wheel"}; + private static final String APOSTROPHE = "'"; + private static final String PIPENV = "pipenv"; + private static final String GRAPH = "graph"; + private static final String RUN = "run"; + private static final String DEV = "--dev"; + private static final String LOCK = "lock"; + /* --- Constructors --- */ + + public PythonDependencyCollector(String pythonPath, String pipPath, boolean installVirtualEnv, boolean resolveHierarchyTree, boolean ignorePipInstallErrors, + String requirementsTxtOrSetupPyPath, String tempDirPackages, String tempDirVirtualEnv, String tempDirDirectPackages) { + super(); + this.pythonPath = pythonPath; + this.x = pipPath; + this.installVirtualEnv = installVirtualEnv; + this.resolveHierarchyTree = resolveHierarchyTree; + if (requirementsTxtOrSetupPyPath.endsWith(Constants.SETUP_PY)) { + requirementsTxtOrSetupPyPath = requirementsTxtOrSetupPyPath.substring(0, requirementsTxtOrSetupPyPath.length() - (Constants.SETUP_PY.length() + 1)); + this.dependencyFileType = DependenciesFileType.SETUP_PY; + } else { + this.dependencyFileType = DependenciesFileType.REQUIREMENTS_TXT; + } + this.requirementsTxtOrSetupPyPath = requirementsTxtOrSetupPyPath; + this.tempDirPackages = tempDirPackages; + this.tempDirVirtualenv = tempDirVirtualEnv; + this.tempDirDirectPackages = tempDirDirectPackages; + this.ignorePipInstallErrors = ignorePipInstallErrors; + } + + public PythonDependencyCollector(boolean ignorePipEnvInstallErrors, boolean runPipEnvPreStep, String tempDirPackages, String pythonPath, String x, boolean pipenvInstallDevDependencies) { + super(); + this.pythonPath = pythonPath; + this.x = x; + this.ignorePipEnvInstallErrors = ignorePipEnvInstallErrors; + this.dependencyFileType = DependenciesFileType.PIPFILE; + this.tempDirPackages = tempDirPackages; + this.runPipEnvPreStep = runPipEnvPreStep; + this.pipenvInstallDevDependencies = pipenvInstallDevDependencies; + } + + @Override + public Collection collectDependencies(String topLevelFolder) { + this.topLevelFolder = topLevelFolder; + List dependencies; + if (this.dependencyFileType.equals(DependenciesFileType.PIPFILE)) { + logger.debug("Found pipfile, running pipenv algorithm"); + dependencies = runPipEnvAlgorithm(); + } else { + logger.debug("Found requiremrents or setup file, running pip algorithm"); + dependencies = runPipAlgorithm(); + } + return getSingleProjectList(dependencies); + } + + private List runPipAlgorithm() { + List dependencies = new LinkedList<>(); + boolean failed = false; + boolean virtualEnvInstalled = true; + if (this.installVirtualEnv && this.resolveHierarchyTree) { + try { + // install virtualEnv package + virtualEnvInstalled = !processCommand(new String[]{pythonPath, M, x, INSTALL, USER, VIRTUALENV}, true); + } catch (IOException e) { + virtualEnvInstalled = false; + } + } + //FSA will run 'pip download -r requirements.txt -d TEMP_FOLDER_PATH' + if (virtualEnvInstalled) { + try { + logger.debug("Collecting python dependencies. It might take a few minutes."); + boolean failedGetTree; + if (this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + failed = processCommand(new String[]{x, DOWNLOAD, R_PARAMETER, this.requirementsTxtOrSetupPyPath, D_PARAMETER, tempDirPackages}, true); + } else if (this.dependencyFileType == DependenciesFileType.SETUP_PY) { + failed = processCommand(new String[]{x, DOWNLOAD, this.requirementsTxtOrSetupPyPath, D_PARAMETER, tempDirPackages}, true); + } + if (failed) { + String error = null; + if (this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + error = "Fail to run 'pip install -r " + this.requirementsTxtOrSetupPyPath + APOSTROPHE; + } else if (this.dependencyFileType == DependenciesFileType.SETUP_PY) { + error = "Fail to run 'pip install " + this.requirementsTxtOrSetupPyPath + APOSTROPHE; + } + logger.warn("{}. To see the full error, re-run the plugin with this parameter in the config file: log.level=debug", error); + } else if (!failed && !this.resolveHierarchyTree) { + dependencies = collectDependencies(new File(tempDirPackages), this.requirementsTxtOrSetupPyPath); + } else if (!failed && this.resolveHierarchyTree) { + failedGetTree = getTree(this.requirementsTxtOrSetupPyPath); + if (!failedGetTree) { + dependencies = collectDependenciesWithTree(this.tempDirVirtualenv + HIERARCHY_TREE_TXT, requirementsTxtOrSetupPyPath); + // the library pipdeptree removes the direct dependencies if those dependencies are transitive dependencies in other dependencies. fixDependencies() returns the direct dependencies + // This issue is not relevant to setup.py dependency file type + if (this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + fixDependencies(dependencies); + } + } else { + // collect flat list if hierarchy tree failed + dependencies = collectDependencies(new File(tempDirPackages), this.requirementsTxtOrSetupPyPath); + } + } + // If there was an error and the dependency file type is requirements.txt, download each dependency in the requirements.txt file one by one + if (failed && this.ignorePipInstallErrors && this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + logger.info("Try to download each dependency in " + this.requirementsTxtOrSetupPyPath + " file one by one. It might take a few minutes."); + FilesUtils.deleteDirectory(new File(tempDirPackages)); + this.tempDirPackages = new FilesUtils().createTmpFolder(false, TempFolders.UNIQUE_PYTHON_TEMP_FOLDER); + if (this.tempDirPackages != null) { + downloadLineByLine(this.requirementsTxtOrSetupPyPath); + dependencies = collectDependencies(new File(tempDirPackages), this.requirementsTxtOrSetupPyPath); + FilesUtils.deleteDirectory(new File(tempDirPackages)); + } + } + } catch (IOException e) { + logger.warn("Cannot read the requirements.txt file"); + } + } else { + logger.warn("Virutalenv package installation failed"); + } + return dependencies; + } + + private List runPipEnvAlgorithm() { + List dependencies = new LinkedList<>(); + boolean failed ; + Set dependencyNamesVersions; + // if to run pipenv install + if (runPipEnvPreStep) { + // if to install dev dependencies or not + logger.debug("Running PipEnv PreStep"); + if (pipenvInstallDevDependencies) { + runPipEnvInstallCommand(new String[]{PIPENV, INSTALL, DEV}); + } else { + runPipEnvInstallCommand(new String[]{PIPENV, INSTALL}); + } + } + logger.info("Running dependency tree with 'pipenv graph'"); + //create requirements.txt temp file in order to be able to download packages + String requirementsTempFile = tempDirPackages + Constants.BACK_SLASH + Constants.PYTHON_REQUIREMENTS; + List lines; + lines = commandLineRunner(topLevelFolder, new String[]{PIPENV, GRAPH}); + if (!CollectionUtils.isEmpty(lines)) { + logger.info("Parsing dependency tree"); + dependencyNamesVersions = parsePipEnvGraph(lines); + } else { + // (pipfile lock -r , pipfile lock -r --dev) = pipenv graph + //pipenv graph picks from the virtual environment, pipfile lock picks dependencies from the pipfile.lock + logger.warn("pipenv graph failed, getting dependencies directly from pipfile"); + lines = commandLineRunner(topLevelFolder, new String[]{PIPENV, LOCK, R_PARAMETER, DEV}); + List lines1 = commandLineRunner(topLevelFolder, new String[]{PIPENV, LOCK, R_PARAMETER}); + lines.addAll(lines1); + dependencyNamesVersions = parsePipFile(lines); + } + + if (dependencyNamesVersions != null) { + try (FileWriter fw = new FileWriter(new File(requirementsTempFile))) { + for (String dependencyNamesVersion : dependencyNamesVersions) { + fw.write(dependencyNamesVersion + "\n"); + } + } catch (IOException e) { + logger.warn("Cannot create a file to write in temp folder {}", e.getMessage()); + logger.debug("Cannot create a file to write in temp folder, Error: {}", e.getStackTrace()); + } + } else { + logger.warn("pipenv graph anad pipfile.lock failed please try to turn python.runPipenvPreStep to run pipenv install"); + } + //create a requirements.txt file from parsing pipenv graph + try { + logger.info("downloading packages"); + failed = processCommand(new String[]{PIPENV, RUN, x, DOWNLOAD, R_PARAMETER, requirementsTempFile, D_PARAMETER, tempDirPackages}, true); + if (failed && ignorePipEnvInstallErrors) { + logger.info("Failed to download all dependencies at once, Try to install dependencies one by one. It might take a few minutes."); + for (String dependencyNamesVersion : dependencyNamesVersions) { + failed = processCommand(new String[]{PIPENV, RUN, x, DOWNLOAD, dependencyNamesVersion, D_PARAMETER, tempDirPackages}, true); + if (failed) { + logger.warn("pipenv run pip download {} failed to execute", dependencyNamesVersion); + } + } + } + dependencies = collectDependencies(new File(tempDirPackages), topLevelFolder); + } catch (IOException e) { + logger.warn("downloading dependencies failed: {}", e.getMessage()); + logger.debug("downloading dependencies failed: {}", e.getStackTrace()); + } + return dependencies; + } + + private Set parsePipEnvGraph(List lines) { + Set dependencyLines = new HashSet<>(); + for (String line : lines) { + logger.debug(line); + if(line.contains(Constants.DOUBLE_EQUALS)) { + dependencyLines.add(line); + } else { + //- execnet [required: >=1.1, installed: 1.5.0] -> convert to execnet==1.5.0 + String artifactId = line.substring(line.indexOf(Constants.DASH) + 2, line.indexOf(Constants.OPEN_SQUARE_BRACKET) - 1); + String version = line.substring(line.lastIndexOf(Constants.WHITESPACE) + 1, line.indexOf(Constants.CLOSE_SQUARE_BRACKET)); + dependencyLines.add(artifactId + Constants.DOUBLE_EQUALS + version); + } + } + return dependencyLines; + } + + private void fixDependencies(Collection dependencies) { + logger.debug("Trying to get all the direct dependencies."); + boolean failed = false; + try { + // get direct dependencies + if (this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + failed = processCommand(new String[]{x, DOWNLOAD, R_PARAMETER, this.requirementsTxtOrSetupPyPath, NO_DEPS, D_PARAMETER, tempDirDirectPackages}, true); + } + if (!failed) { + findDirectDependencies(dependencies); + } else { + logger.debug("Cannot download direct dependencies."); + } + } catch (IOException e) { + logger.debug("Cannot download direct dependencies."); + } + } + + private void findDirectDependencies(Collection dependencies) { + File directDependenciesFolder = new File(this.tempDirDirectPackages); + File[] directDependenciesFiles = directDependenciesFolder.listFiles(); + if (directDependenciesFiles.length > dependencies.size()) { + int missingDirectDependencies = directDependenciesFiles.length - dependencies.size(); + logger.debug("There are {} missing direct dependencies", missingDirectDependencies); + int i = 0; + while (missingDirectDependencies > 0) { + boolean found = false; + File directDependencyToCheck = directDependenciesFiles[i]; + for (DependencyInfo dependency : dependencies) { + if (dependency.getArtifactId().equals(directDependencyToCheck.getName())) { + found = true; + break; + } + } + if (!found) { + logger.debug("Trying to find the direct dependency of: {}", directDependencyToCheck.getName()); + DependencyInfo foundDependencyInfo = findDirectDependencyInTree(dependencies, directDependencyToCheck); + if (foundDependencyInfo != null) { + dependencies.add(foundDependencyInfo); + } else { + // probably issue with pipdeptree (maybe cyclic dependency) + logger.warn("Error getting dependency {}, might be issues using pipdeptree command", directDependencyToCheck.getName()); + } + missingDirectDependencies--; + } + i++; + } + } + } + + private DependencyInfo findDirectDependencyInTree(Collection dependencies, File directDependencyToCheck) { + for (DependencyInfo dependency : dependencies) { + if (dependency.getArtifactId().equals(directDependencyToCheck.getName())) { + return dependency; + } else { + DependencyInfo child = findDirectDependencyInTree(dependency.getChildren(), directDependencyToCheck); + if (child != null) { + return child; + } + } + } + return null; + } + + private List collectDependenciesWithTree(String treeFile, String requirementsTxtPath) { + List dependencies = new LinkedList<>(); + try { + // read json dependency tree from cmd tmp file + String allTreeFile = new String(Files.readAllBytes(Paths.get(treeFile)), StandardCharsets.UTF_8); + JSONArray treeArray = new JSONArray(allTreeFile); + File[] files = (new File(this.tempDirPackages)).listFiles(); + if (this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + dependencies = collectDependenciesReq(treeArray, files, requirementsTxtPath); + } else if (this.dependencyFileType == DependenciesFileType.SETUP_PY) { + dependencies = collectDependenciesReq(treeArray.getJSONObject(findIndexInArrayOfPipdeptree(treeArray)).getJSONArray(DEPENDENCIES), files, requirementsTxtPath); + } + } catch (IOException e) { + logger.warn("Cannot read the hierarchy tree file"); + } + return dependencies; + } + + private int findIndexInArrayOfPipdeptree(JSONArray treeArray) { + for (int i = 0; i < treeArray.length(); i++) { + boolean findDefault = false; + String packageName = treeArray.getJSONObject(i).getString(PACKAGE_NAME); + for (String defaultPackage : DEFAULT_PACKAGES_IN_PIPDEPTREE) { + if (defaultPackage.equals(packageName)) { + findDefault = true; + break; + } + } + if (!findDefault) { + return i; + } + } + return treeArray.length() - 1; + } + + private List collectDependenciesReq(JSONArray dependenciesArray, File[] files, String requirementsTxtPath) { + List dependencies = new LinkedList<>(); + for (int i = 0; i < dependenciesArray.length(); i++) { + JSONObject packageObject = dependenciesArray.getJSONObject(i); + DependencyInfo dependency = getDependencyByName(files, packageObject.getString(PACKAGE_NAME), packageObject.getString(INSTALLED_VERSION), requirementsTxtPath); + if (dependency != null) { + dependencies.add(dependency); + dependency.setChildren(collectDependenciesReq(packageObject.getJSONArray(DEPENDENCIES), files, requirementsTxtPath)); + } + } + return dependencies; + } + + private DependencyInfo getDependencyByName(File[] files, String name, String version, String requirementsTxtPath) { + String nameAndVersion1 = name + COMMA + version; + String nameAndVersion2 = name.replace(COMMA, LOWER_COMMA) + COMMA + version; + nameAndVersion1 = nameAndVersion1.toLowerCase(); + nameAndVersion2 = nameAndVersion2.toLowerCase(); + for (File file : files) { + String fileNameLowerCase = file.getName().toLowerCase(); + if (fileNameLowerCase.startsWith(nameAndVersion1) || fileNameLowerCase.startsWith(nameAndVersion2)) { + return getDependencyFromFile(file, requirementsTxtPath); + } + } + return null; + } + + private boolean getTree(String requirementsTxtPath) { + boolean failed; + try { + // Create the virtual environment + failed = processCommand(new String[]{pythonPath, M, VIRTUALENV, this.tempDirVirtualenv + ENV}, true); + if (!failed) { + if (isWindows()) { + failed = processCommand(getFullCmdInstallation(requirementsTxtPath), true); + } else { + String scriptPath = createScript(requirementsTxtPath); + if (scriptPath != null) { + failed = processCommand(new String[] {scriptPath}, true); + } else { + failed = true; + } + } + } + } catch (IOException e) { + logger.warn("Cannot install requirements.txt in the virtual environment."); + failed = true; + } + return failed; + } + + private List collectDependencies(File folder, String requirementsTxtPath) { + List result = new LinkedList<>(); + for (File file : folder.listFiles()) { + if (file.isDirectory()) { + for (File regFile : file.listFiles()) { + addDependencyInfoData(regFile, requirementsTxtPath, result); + } + } else { + addDependencyInfoData(file, requirementsTxtPath, result); + } + } + return result; + } + + private void addDependencyInfoData(File file, String requirementsTxtPath, List dependencies) { + DependencyInfo dependency = getDependencyFromFile(file, requirementsTxtPath); + if (dependency != null) { + dependencies.add(dependency); + } + } + + private DependencyInfo getDependencyFromFile(File file, String requirementsTxtPath) { + DependencyInfo dependency = new DependencyInfo(); + String fileName = file.getName(); + // ignore name and version and use only the sha1 + +// int firstIndexOfComma = fileName.indexOf(COMMA); +// String name = fileName.substring(0, firstIndexOfComma); +// int indexOfExtension = fileName.indexOf(TAR_GZ); +// String version; +// if (indexOfExtension < 0) { +// indexOfExtension = fileName.lastIndexOf(DOT); +// version = fileName.substring(firstIndexOfComma + 1, indexOfExtension); +// int indexOfComma = version.indexOf(COMMA); +// if (indexOfComma >= 0) { +// version = version.substring(0, indexOfComma); +// } +// } else { +// version = fileName.substring(firstIndexOfComma + 1, indexOfExtension); +// } + String sha1 = getSha1(file); + if (StringUtils.isEmpty(sha1)) { + return null; + } +// dependency.setGroupId(name); + dependency.setArtifactId(fileName); +// dependency.setVersion(version); + dependency.setSha1(sha1); + dependency.setSystemPath(requirementsTxtPath); + dependency.setDependencyType(DependencyType.PYTHON); + return dependency; + } + + private String getSha1(File file) { + try { + return ChecksumUtils.calculateSHA1(file); + } catch (IOException e) { + logger.warn("Failed getting. {} File will not be send to WhiteSource server.", file); + return EMPTY_STRING; + } + } + + private String[] getFullCmdInstallation(String requirementsTxtPath) { + // execute all the command with and between them in order to save the virtualenv shell + String[] windowsCommand = null; + if (this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + windowsCommand = new String[]{this.tempDirVirtualenv + SCRIPTS_ACTIVATE, AND, x, INSTALL, R_PARAMETER, + requirementsTxtPath, F, this.tempDirPackages, AND, x, INSTALL, PIPDEPTREE, AND, PIPDEPTREE, + JSON_TREE, ARROW, this.tempDirVirtualenv + HIERARCHY_TREE_TXT}; + } else if (this.dependencyFileType == DependenciesFileType.SETUP_PY) { + windowsCommand = new String[]{this.tempDirVirtualenv + SCRIPTS_ACTIVATE, AND, x, INSTALL, + requirementsTxtPath, F, this.tempDirPackages, AND, x, INSTALL, PIPDEPTREE, AND, PIPDEPTREE, + JSON_TREE, ARROW, this.tempDirVirtualenv + HIERARCHY_TREE_TXT}; + } + return windowsCommand; + } + + private boolean processCommand(String[] args, boolean withOutput) throws IOException { + CommandLineProcess commandLineProcess = new CommandLineProcess(this.topLevelFolder, args); + if (withOutput) { + commandLineProcess.executeProcess(); + } else { + commandLineProcess.executeProcessWithoutOutput(); + } + return commandLineProcess.isErrorInProcess(); + } + + private List commandLineRunner(String rootDirectory, String[] params) { + try { + // run pipenv graph + CommandLineProcess commandLineProcess = new CommandLineProcess(rootDirectory, params); + List lines = commandLineProcess.executeProcess(); + if (commandLineProcess.isErrorInProcess()) { + logger.warn("error in process after running command {} on {}", params, rootDirectory); + } else { + return lines; + } + } catch (IOException e) { + logger.warn("Error getting results after running command {} on {}, {}", params, rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + return Collections.emptyList(); + } + + private Set parsePipFile(List lines) { + Set DependencyLines = new HashSet<>(); + for (String line : lines) { + if (!line.contains(Constants.WHITESPACE)) { + DependencyLines.add(line); + } else { + if (line.contains(Constants.DOUBLE_EQUALS)) { + String artifactAndVersion = line.substring(0, line.indexOf(Constants.SEMI_COLON)); + DependencyLines.add(artifactAndVersion); + } + } + } + return DependencyLines; + } + + private void downloadLineByLine(String requirementsTxtPath) { + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(new File(requirementsTxtPath)))){ + ExecutorService executorService = Executors.newWorkStealingPool(NUM_THREADS); + Collection threadsCollection = new LinkedList<>(); + String line; + while ((line = bufferedReader.readLine()) != null) { + if (StringUtils.isNotEmpty(line)) { + int commentIndex = line.indexOf(COMMENT_SIGN_PYTHON); + if (commentIndex < 0) { + commentIndex = line.length(); + } + String packageNameToDownload = line.substring(0, commentIndex); + if (StringUtils.isNotEmpty(packageNameToDownload)) { + threadsCollection.add(new DownloadDependency(packageNameToDownload)); + } + } + } + runThreadCollection(executorService, threadsCollection); + } catch (IOException e) { + logger.warn("Cannot read the requirements.txt file: {}", e.getMessage()); + } + } + + private void runThreadCollection(ExecutorService executorService, Collection threadsCollection) { + try { + executorService.invokeAll(threadsCollection); + executorService.shutdown(); + } catch (InterruptedException e) { + logger.warn("One of the threads was interrupted, please try to scan again the project. Error: {}", e.getMessage()); + logger.debug("One of the threads was interrupted, please try to scan again the project. Error: {}", e.getStackTrace()); + } + } + + private void downloadOneDependency(String packageName) { + int currentCounter = this.counterFolders.incrementAndGet(); + String message = "Failed to download the transitive dependencies of '"; + try { + if (processCommand(new String[]{ + x, DOWNLOAD, packageName, D_PARAMETER, tempDirPackages + FORWARD_SLASH + currentCounter}, false)) { + logger.warn(message + packageName + "'"); + } + } catch (IOException e) { + logger.warn("Cannot read the requirements.txt file"); + } + } + + private String createScript(String requirementsTxtPath) { + FilesUtils filesUtils = new FilesUtils(); + String path = filesUtils.createTmpFolder(false, TempFolders.UNIQUE_PYTHON_TEMP_FOLDER); + String scriptPath = null; + if (path != null) { + scriptPath = path + SCRIPT_SH; + File file = new File(scriptPath); + try ( FileOutputStream fos = new FileOutputStream(file); + BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(fos))) { + bufferedWriter.write(BIN_BASH); + bufferedWriter.newLine(); + bufferedWriter.write(SOURCE + Constants.WHITESPACE + this.tempDirVirtualenv + BIN_ACTIVATE); + bufferedWriter.newLine(); + if (this.dependencyFileType == DependenciesFileType.REQUIREMENTS_TXT) { + bufferedWriter.write( + x + Constants.WHITESPACE + INSTALL + Constants.WHITESPACE + R_PARAMETER + + Constants.WHITESPACE + requirementsTxtPath + Constants.WHITESPACE + F + Constants.WHITESPACE + this.tempDirPackages); + } else if (this.dependencyFileType == DependenciesFileType.SETUP_PY) { + bufferedWriter.write(x + Constants.WHITESPACE + INSTALL + Constants.WHITESPACE + + Constants.WHITESPACE + requirementsTxtPath + Constants.WHITESPACE + F + Constants.WHITESPACE + this.tempDirPackages); + } + bufferedWriter.newLine(); + bufferedWriter.write(x + Constants.WHITESPACE + INSTALL + Constants.WHITESPACE + PIPDEPTREE); + bufferedWriter.newLine(); + bufferedWriter.write(PIPDEPTREE + Constants.WHITESPACE + JSON_TREE + Constants.WHITESPACE + ARROW + Constants.WHITESPACE + this.tempDirVirtualenv + HIERARCHY_TREE_TXT); + file.setExecutable(true); + } catch (IOException e) { + return null; + } + } + return scriptPath; + } + + + /* --- Nested classes --- */ + + class DownloadDependency implements Callable { + + /* --- Members --- */ + + private String packageName; + + /* --- Constructors --- */ + + public DownloadDependency(String packageName) { + this.packageName = packageName; + } + + /* --- Overridden methods --- */ + + @Override + public Void call() { + downloadOneDependency(this.packageName); + return null; + } + } + private void runPipEnvInstallCommand(String[] args){ + + try { + ProcessBuilder builder = new ProcessBuilder(); + builder.command(args); + builder.directory(new File(this.topLevelFolder)); + Process process = builder.start(); + StreamGobbler streamGobbler = new StreamGobbler(process.getInputStream(), System.out::println); + Executors.newSingleThreadExecutor().submit(streamGobbler); + //for debug mode, to check errors + BufferedReader inputStreamPipEnvInstall = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedReader errorStreamPipEnvInstall = new BufferedReader(new InputStreamReader(process.getErrorStream())); + String cmdOutput; + while ((cmdOutput = inputStreamPipEnvInstall.readLine()) != null) { + logger.debug(cmdOutput); + } + while ((cmdOutput = errorStreamPipEnvInstall.readLine()) != null) { + logger.debug(cmdOutput); + } + int exitCode = 0; + exitCode = process.waitFor(); + logger.debug("Exit Code: {}", exitCode); + } catch (IOException e){ + logger.warn("IOException: {}", e.getMessage()); + logger.debug("IOException: {}", e.getStackTrace()); + } catch (InterruptedException e) { + logger.warn("Interrupted Exception: {}", e.getMessage()); + logger.debug("Interrupted Exception: {}", e.getStackTrace()); + } + } + private static class StreamGobbler implements Runnable { + private InputStream inputStream; + private Consumer consumer; + + public StreamGobbler(InputStream inputStream, Consumer consumer) { + this.inputStream = inputStream; + this.consumer = consumer; + } + @Override + public void run() { + new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(consumer); + } + } +} + diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyResolver.java new file mode 100644 index 0000000..7442f80 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/python/PythonDependencyResolver.java @@ -0,0 +1,195 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.dependency.resolver.python; + +import org.whitesource.agent.Constants; +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.dependency.resolver.dotNet.RestoreCollector; +import org.whitesource.agent.utils.FilesUtils; + +import java.io.File; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +public class PythonDependencyResolver extends AbstractDependencyResolver { + + + + /* -- Members -- */ + + private final String pythonPath; + private final String pipPath; + private final boolean ignoreSourceFiles; + private final boolean ignorePipEnvInstallErrors; + private final boolean runPipenvPreStep; + private final boolean pipenvInstallDevDependencies; + private Collection excludes = new ArrayList<>(); + private boolean ignorePipInstallErrors; + private boolean installVirutalenv; + private boolean resolveHierarchyTree; + private String[] pythonRequirementsFileIncludes; + public String PYTHON_REGEX = "\\\\"; + /* --- Static members --- */ + + //private static final String PYTHON_BOM = "requirements.txt"; + private static final String PY_EXT = ".py"; + public static final String DIRECT = "_direct"; + + /* --- Constructors --- */ + + public PythonDependencyResolver(String pythonPath, String pipPath, boolean ignorePipInstallErrors, + boolean installVirtualEnv, boolean resolveHierarchyTree, String[] pythonRequirementsFileIncludes, boolean ignoreSourceFiles, boolean ignorePipEnvInstallErrors, boolean runPipenvPreStep, boolean pipenvInstallDevDependencies) { + super(); + this.pythonPath = pythonPath; + this.pipPath = pipPath; + this.ignorePipInstallErrors = ignorePipInstallErrors; + this.installVirutalenv = installVirtualEnv; + this.resolveHierarchyTree = resolveHierarchyTree; + this.pythonRequirementsFileIncludes = pythonRequirementsFileIncludes; + this.ignoreSourceFiles = ignoreSourceFiles; + this.ignorePipEnvInstallErrors = ignorePipEnvInstallErrors; + this.runPipenvPreStep = runPipenvPreStep; + this.pipenvInstallDevDependencies = pipenvInstallDevDependencies; + } + + @Override + public ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set dependenciesFiles) { + + if (ignoreSourceFiles) { + this.excludes = Arrays.asList(Constants.PATTERN + PY_EXT); + } + FilesUtils filesUtils = new FilesUtils(); + Collection resultDependencies = new LinkedList<>(); + Collection dependencyInfos = new LinkedList<>(); + String pipFilePath = projectFolder + RestoreCollector.BACK_SLASH + Constants.PIPFILE; + //check if Pipfile exists, then use pipenv, else use pip + if (Paths.get(pipFilePath).toFile().exists()) { + resultDependencies = runPipEnvAlgorithm(filesUtils, pipFilePath); + } else { + dependencyInfos = runPipAlgorithm(filesUtils, dependenciesFiles); + resultDependencies.addAll(dependencyInfos); + } + return new ResolutionResult(resultDependencies, getExcludes(), getDependencyType(), topLevelFolder); + } + + private Collection runPipAlgorithm(FilesUtils filesUtils, Set dependenciesFiles) { + LinkedList resultDependencies = new LinkedList<>(); + for (String dependencyFile : dependenciesFiles) { + String tempDirVirtualEnv = filesUtils.createTmpFolder(true, TempFolders.UNIQUE_PYTHON_TEMP_FOLDER); + String tempDirPackages = filesUtils.createTmpFolder(false, TempFolders.UNIQUE_PYTHON_TEMP_FOLDER); + String tempDirDirectPackages = filesUtils.createTmpFolder(false, TempFolders.UNIQUE_PYTHON_TEMP_FOLDER + DIRECT); + PythonDependencyCollector pythonDependencyCollector; + Collection dependencies = new LinkedList<>(); + if (tempDirVirtualEnv != null && tempDirPackages != null) { + pythonDependencyCollector = new PythonDependencyCollector(this.pythonPath, this.pipPath, this.installVirutalenv, this.resolveHierarchyTree, this.ignorePipInstallErrors, + dependencyFile, tempDirPackages, tempDirVirtualEnv, tempDirDirectPackages); + String currentTopLevelFolder = dependencyFile.substring(0, dependencyFile.replaceAll(PYTHON_REGEX, + Constants.FORWARD_SLASH).lastIndexOf(Constants.FORWARD_SLASH)); + Collection projects = pythonDependencyCollector.collectDependencies(currentTopLevelFolder); + dependencies = projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()); + // delete tmp folders + FilesUtils.deleteDirectory(new File(tempDirVirtualEnv)); + FilesUtils.deleteDirectory(new File(tempDirPackages)); + FilesUtils.deleteDirectory(new File(tempDirDirectPackages)); + } + resultDependencies.addAll(dependencies); + } + return resultDependencies; + } + + private Collection runPipEnvAlgorithm(FilesUtils filesUtils, String pipfilePath) { + String tempDirPackages = null; + Collection dependencies = new LinkedList<>(); + try { + tempDirPackages = filesUtils.createTmpFolder(true, TempFolders.UNIQUE_PYTHON_TEMP_FOLDER); + String dependencyFile = pipfilePath; + PythonDependencyCollector pythonDependencyCollector; + pythonDependencyCollector = new PythonDependencyCollector(ignorePipEnvInstallErrors, runPipenvPreStep, tempDirPackages, pythonPath, pipPath, pipenvInstallDevDependencies); + String currentTopLevelFolder = dependencyFile.substring(0, dependencyFile.replaceAll(PYTHON_REGEX, Constants.FORWARD_SLASH).lastIndexOf(Constants.FORWARD_SLASH)); + Collection projects = pythonDependencyCollector.collectDependencies(currentTopLevelFolder); + dependencies = projects.stream().flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()); + } finally { + if (tempDirPackages != null) { + FilesUtils.deleteDirectory(new File(tempDirPackages)); + } + } + return dependencies; + } + + @Override + protected Collection getExcludes() { + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + List stringList = Arrays.asList(pythonRequirementsFileIncludes); + stringList.stream().forEach(s -> s = Constants.PATTERN + s); + return stringList; + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.PYTHON; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.PYTHON.name(); + } + + @Override + public String[] getBomPattern() { + List stringList = new ArrayList<>(Arrays.asList(pythonRequirementsFileIncludes)); + for (int i = 0; i < stringList.size(); i++) { + stringList.set(i, Constants.PATTERN + stringList.get(i)); + } + return stringList.toArray(new String[0]); + } + + @Override + public Collection getManifestFiles() { + return Arrays.asList(pythonRequirementsFileIncludes); + } + + @Override + protected Collection getLanguageExcludes() { + return new ArrayList<>(); + } + + /* --- Getters / Setters --- */ + + public String getPythonPath() { + return pythonPath; + } + + public String getPipPath() { + return pipPath; + } + + @Override + protected Collection getRelevantScannedFolders(Collection scannedFolders) { + // Python resolver should scan all folders and should not remove any folder + return scannedFolders == null ? Collections.emptyList() : scannedFolders; + } +} + diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyCli.java b/src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyCli.java new file mode 100644 index 0000000..2c386e1 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyCli.java @@ -0,0 +1,30 @@ +package org.whitesource.agent.dependency.resolver.ruby; + +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.utils.Cli; + +public class RubyCli extends Cli { +// TODO - this class can be removed (the functionality here is incorporated into this method in Cli class) + @Override + public String[] getCommandParams(String command, String param){ + String[] params = param.split(Constants.WHITESPACE); + String[] output; + if (DependencyCollector.isWindows()) { + output = new String[3 + params.length]; + output[0] = Constants.CMD; + output[1] = DependencyCollector.C_CHAR_WINDOWS; + output[2] = command; + for (int i = 0; i < params.length; i++){ + output[i + 3] = params[i]; + } + } else { + output = new String[1 + params.length]; + output[0] = command; + for (int i = 0; i < params.length; i++){ + output[i + 1] = params[i]; + } + } + return output; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyDependencyResolver.java new file mode 100644 index 0000000..36c3be2 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/ruby/RubyDependencyResolver.java @@ -0,0 +1,658 @@ +package org.whitesource.agent.dependency.resolver.ruby; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.hash.ChecksumUtils; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.*; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.whitesource.agent.dependency.resolver.ruby.RubyDependencyResolver.GEM; + +public class RubyDependencyResolver extends AbstractDependencyResolver { + + private static final String GEM_FILE = "Gemfile"; + private static final String GEMS_RB = "gems.rb"; + private static final String GEM_FILE_LOCK = "Gemfile.lock"; + private static final String GEMS_LOCKED = "gems.locked"; + private static final String ORIG = ".orig"; + private static final String BUNDLE = "bundle"; + private static final String ENVIRONMENT = "environment gemdir"; + private static final char TILDE = '~'; + protected static final String GEM = "gem"; + protected static final String REGEX = "\\S"; + protected static final String SPECS = "specs:"; + protected static final String CACHE = "cache"; + protected static final String V = "-v"; + protected static final String ERROR = "ERROR"; + protected static final String MINGW = "mingw"; + + private static final List RUBY_SCRIPT_EXTENSION = Arrays.asList(".rb"); + private final Logger logger = LoggerFactory.getLogger(RubyDependencyResolver.class); + private final boolean ignoreSourceFiles; + + private RubyCli cli; + private boolean runBundleInstall; + private boolean overwriteGemFile; + private boolean installMissingGems; + private String rootDirectory; + + public RubyDependencyResolver(boolean runBundleInstall, boolean overwriteGemFile, boolean installMissingGems, boolean ignoreSourceFiles) { + super(); + cli = new RubyCli(); + this.runBundleInstall = runBundleInstall; + this.overwriteGemFile = overwriteGemFile; + this.installMissingGems = installMissingGems; + this.ignoreSourceFiles = ignoreSourceFiles; + } + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + rootDirectory = topLevelFolder; + List dependencies = collectDependencies(); + return new ResolutionResult(dependencies, getExcludes(), getDependencyType(), topLevelFolder); + } + + @Override + protected Collection getExcludes() { + Set excludes = new HashSet<>(); + if (ignoreSourceFiles) { + for (String rubyExtension : RUBY_SCRIPT_EXTENSION) { + if (rubyExtension.equalsIgnoreCase(".rb")) { + excludes.add(Constants.PATTERN + "!(gems)" + rubyExtension); + } else { + excludes.add(Constants.PATTERN + rubyExtension); + } + } + } + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return RUBY_SCRIPT_EXTENSION; + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.RUBY; + } + + @Override + protected String getDependencyTypeName() { + return DependencyType.RUBY.name(); + } + + @Override + public String[] getBomPattern() { + return new String[]{Constants.PATTERN + GEM_FILE_LOCK, + Constants.PATTERN + GEM_FILE, Constants.PATTERN + GEMS_RB, Constants.PATTERN + GEMS_LOCKED}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(GEM_FILE_LOCK, GEM_FILE, GEMS_RB, GEMS_LOCKED); + } + + @Override + protected Collection getLanguageExcludes() { + return null; + } + + private List collectDependencies() { + List dependencyInfos = new ArrayList<>(); + File gemsLocked = new File(rootDirectory + fileSeparator + GEMS_LOCKED); + File gemFileLock = new File(rootDirectory + fileSeparator + GEM_FILE_LOCK); + + String gemsFileName = getGemsFileName(); + + // Run bundle install will create gems.locked/Gemfile.lock + if (runBundleInstall) { + runBundleInstall(gemsFileName); + } + + // Parse both gems.locked and Gemfile.lock if exists + // Gems files gems.locked and Gemfile.lock have the same content syntax, So they are parsed in the same method + if (gemsLocked.isFile()) { + parseGemsFile(gemsLocked, dependencyInfos); + } + if (gemFileLock.isFile()) { + parseGemsFile(gemFileLock, dependencyInfos); + } + + if (!gemsLocked.isFile() && !gemFileLock.isFile()) { + // actually we should never reach here - if gems.locked and gemFile.lock aren't found the RubyDependencyResolver won't run + logger.warn("Ruby gems files {} and {} doesn't exist. Nothing to scan in {}", GEMS_LOCKED, GEM_FILE_LOCK, rootDirectory); + } + + if (runBundleInstall && gemsFileName != null) { + File gemsFile = new File(rootDirectory + fileSeparator + gemsFileName); + File gemsFileOrig = new File(rootDirectory + fileSeparator + gemsFileName + ORIG); + if (gemsFileOrig.isFile()) { + removeTempFile(gemsFile, gemsFileOrig); + } + } + + return dependencyInfos; + } + + /** + * Get gems file name {gems.locked / Gemfile.lock} that will be created in bundle install command. + * Bundler version < 2 will create 'Gemfile.lock' if 'Gemfile' exist, If 'Gemfile' doesn't exist and 'gems.rb' exist it will create 'gems.locked' + * Bundler version >= 2 will create 'gems.locked' if 'gems.rb' exist, If 'gems.rb' doesn't exist and 'Gemfile' exist it will create 'Gemfile.lock' + * + * @return Gem file that will be created - 'gems.locked' / 'Gemfile.lock' + */ + private String getGemsFileName() { + File gemsRb = new File(rootDirectory + fileSeparator + GEMS_RB); + File gemFile = new File(rootDirectory + fileSeparator + GEM_FILE); + + if (!gemsRb.isFile() && !gemFile.isFile()) { + return null; + } + + List bundleVersionResult = cli.runCmd(rootDirectory, cli.getCommandParams(BUNDLE, Constants.VERSION)); + + if (bundleVersionResult != null) { + if (bundleVersionResult.get(0).contains("Bundler version")) { + Pattern p = Pattern.compile("Bundler version ((?:\\d|\\.)+)"); + Matcher m = p.matcher(bundleVersionResult.get(0)); + if (m.find()) { + String version = m.group(1); + int versionNumber = Integer.parseInt(version.substring(0, version.indexOf('.'))); + if (versionNumber < 2) { + // In version <2 if gems.rb exist and gemFile doesn't exist return "gems.locked" else "gemFile.lock" + if (gemFile.isFile()) { + return GEM_FILE_LOCK; + } else if (gemsRb.isFile()) { + return GEMS_LOCKED; + } + } else { + // In version > 2 if gems.rb exist return "gems.locked" else "gemFile.lock" + if (gemsRb.isFile()) { + return GEMS_LOCKED; + } else if (gemFile.isFile()) { + return GEM_FILE_LOCK; + } + } + } + } + } + return null; + } + + private boolean runBundleInstall(String gemsFileName) { + if (gemsFileName == null) { + logger.warn("Cannot run bundler install, neither {} nor {} exists. Please run bundle init.", GEMS_RB, GEM_FILE); + return false; + } + + File gemsFile = new File(rootDirectory + fileSeparator + gemsFileName); + File gemsFileOrig = new File(rootDirectory + fileSeparator + gemsFileName + ORIG); + + if (!overwriteGemFile && gemsFile.isFile()) { + // rename the original gems.locked/gemFile.lock (if it exists) + gemsFile.renameTo(gemsFileOrig); + } + + boolean bundleInstallSuccess = !cli.runCmd(rootDirectory, cli.getCommandParams(BUNDLE, Constants.INSTALL)).isEmpty() && gemsFile.isFile(); + if (!bundleInstallSuccess && !overwriteGemFile) { + // when running the 'bundle install' command failed and the original file was renamed - restore its name + gemsFileOrig.renameTo(gemsFile); + } + + return bundleInstallSuccess; + } + + private void removeTempFile(File gemsFile, File origGemsFile) { + // when the original gems file gems.locked/Gemfile.lock was renamed - remove the temp file and restore the original file its name + if (origGemsFile.isFile()) { + try { + FileUtils.forceDelete(gemsFile); + origGemsFile.renameTo(gemsFile); + } catch (IOException e) { + logger.warn("can't remove {}: {}", gemsFile.getPath(), e.getMessage()); + } + } + } + + private void parseGemsFile(File gemLockFile, List dependencyInfos) { + /* + * gems.locked/Gemfile.lock (relevant) content structure: + GEM + remote: https://rubygems.org/ + specs: + httparty (0.13.7) + json (~> 1.8) + multi_xml (>= 0.5.2) + json (1.8.6) + kramdown (1.8.0) + multi_xml (0.5.5) + parallel (1.6.1) + * */ + String pathToGems = null; + try { + pathToGems = findPathToGems(); + if (pathToGems == null) { + logger.warn("Can't find path to gems' cache folder"); + return; + } + } catch (FileNotFoundException e) { + logger.warn("Can't find path to gems' cache folder {}", e.getMessage()); + return; + } + FileReader fileReader = null; + BufferedReader bufferedReader = null; + try { + fileReader = new FileReader(gemLockFile); + bufferedReader = new BufferedReader(fileReader); + String currLine; + boolean insideGem = false; + boolean insideSpecs = false; + Pattern pattern = Pattern.compile(REGEX); + Matcher matcher; + Integer previousIndex = 0; + boolean indented = false; + DependencyInfo dependencyInfo = null; + List parentsList = new LinkedList<>(); + List childrenList = new LinkedList<>(); + List partialDependencies = new LinkedList<>(); + DependencyInfo parentDependency = null; + whileLoop: + while ((currLine = bufferedReader.readLine()) != null) { + if (insideGem && insideSpecs) { + if (currLine.isEmpty()) { + break; + } else { + matcher = pattern.matcher(currLine); + if (matcher.find()) { + int index = matcher.start(); + if ((index > previousIndex && previousIndex > 0) || (indented && index == previousIndex)) { + // inside indentation - a child dependency + indented = true; + previousIndex = index; + int spaceIndex = currLine.indexOf(Constants.WHITESPACE, index); + String name = currLine.substring(index, spaceIndex > -1 ? spaceIndex : currLine.length()); + // looking for the dependency in the parents' list + dependencyInfo = parentsList.stream().filter(d -> d.getGroupId().equals(name)).findFirst().orElse(null); + boolean inParentsList = dependencyInfo != null; + if (!inParentsList) { + dependencyInfo = childrenList.stream().filter(d -> d.getGroupId().equals(name)).findFirst().orElse(null); + if (dependencyInfo == null) { + dependencyInfo = new DependencyInfo(); + dependencyInfo.setGroupId(name); + int indexOfOpenBracket = currLine.indexOf(Constants.OPEN_BRACKET); + int indexOfCloseBracket = currLine.indexOf(Constants.CLOSE_BRACKET); + if (indexOfOpenBracket != -1 && indexOfCloseBracket != -1) { + String version = currLine.substring(currLine.indexOf(Constants.OPEN_BRACKET) + 1, currLine.indexOf(Constants.CLOSE_BRACKET)); + int indexSeparator = version.indexOf(Constants.COMMA); + if (indexSeparator != -1) { + version = version.substring(0, indexSeparator); + } + // take only the version number in case of version with tilde. For example: concurrent-ruby (~> 1.0) + if (version.charAt(0) == TILDE) { + dependencyInfo.setVersion(version.substring(3)); + } else { + dependencyInfo.setVersion(version); + } + } + } + } + if (parentDependency != null) { + if (!partialDependencies.contains(dependencyInfo)) { + partialDependencies.add(dependencyInfo); + } + // adding this dependency as a child to its parent + parentDependency.getChildren().add(dependencyInfo); // using loop with `equal` and not `contains` since the contains would fail when the key of a hash map is modified after its creation (as in this case) + // if this dependency is already found in the children's list - continue + for (DependencyInfo d : childrenList) { + if (d.equals(dependencyInfo)) { + continue whileLoop; + } + } + childrenList.add(dependencyInfo); + } else if (!inParentsList) { + // Adding this dependency as a parent although its a child of other dependency. + // This case happens when failed to create parent of this dependency (for example in case parent gem isn't installed) + String version = dependencyInfo.getVersion(); + try { + String sha1 = getRubyDependenciesSha1(name, version, pathToGems); + if (sha1 == null) { + logger.warn("Can't find gem file for {}-{}", name, version); + continue whileLoop; + } + dependencyInfo.setSha1(sha1); + setDependencyInfoProperties(dependencyInfo, name, version, gemLockFile, pathToGems); + parentsList.add(dependencyInfo); + } catch (IOException e) { + logger.warn("Can't find gem file for {}-{}", name, version); + } + } + } else { + // inside a parent dependency + String[] split = currLine.trim().split(Constants.WHITESPACE); + String name = split[0]; + String version = split[1].substring(1, split[1].length() - 1); + try { + String sha1 = getRubyDependenciesSha1(name, version, pathToGems); + if (sha1 == null) { + parentDependency = null; + logger.warn("Can't find gem file for {}-{}", name, version); + continue whileLoop; + } + // looking for this dependency in the children's list (in case its already a child of some other dependency) + dependencyInfo = childrenList.stream().filter(d -> d.getGroupId().equals(name)).findFirst().orElse(null); + if (dependencyInfo == null) { + dependencyInfo = new DependencyInfo(sha1); + dependencyInfo.setGroupId(name); + } else { + dependencyInfo.setSha1(sha1); + } + partialDependencies.remove(dependencyInfo); + setDependencyInfoProperties(dependencyInfo, name, version, gemLockFile, pathToGems); + parentsList.add(dependencyInfo); + parentDependency = dependencyInfo; + } catch (IOException e) { + logger.warn("Can't find gem file for {}-{}", name, version); + } finally { + indented = false; + previousIndex = index; + } + } + } + } + } else if (currLine.contains(GEM.toUpperCase())) { + insideGem = true; + } else if (insideGem && currLine.contains(SPECS)) { + insideSpecs = true; + } + } + // creating the dependencies list by using only the parent dependencies, i.e. - those that aren't found in the children's list + // using loop with `equal` and not `contains` since the contains would fail when the key of a hash map is modified after its creation (as in this case) + for (DependencyInfo parent : parentsList) { + boolean foundChild = false; + for (DependencyInfo child : childrenList) { + if (parent.equals(child)) { + foundChild = true; + break; + } + } + if (!foundChild) { + dependencyInfos.add(parent); + } + } + + // Remove dependency cycle, the case when dependency depend on itself. + removeDependencyCycle(dependencyInfos); + + // partial dependencies are those who appear in the Gemfile.lock only as child dependencies, thus without valid version. + // in such case, remove that dependency from its parent + for (DependencyInfo partialDependency : partialDependencies) { + try { + String version = findGemVersion(partialDependency.getGroupId(), pathToGems); + String versionToCompare = null; + String partialDependencyVersion = partialDependency.getVersion(); + if (partialDependencyVersion != null) { + char firstChar = partialDependencyVersion.charAt(0); + if (firstChar == '>' || firstChar == Constants.EQUALS_CHAR) { + versionToCompare = partialDependencyVersion.substring(partialDependencyVersion.indexOf(' ') + 1); + } + } + if (version == null || (versionToCompare != null && versionCompare(versionToCompare, version) > 0)) { + List lines = installGem(partialDependency.getGroupId(), Constants.APOSTROPHE + partialDependency.getVersion() + Constants.APOSTROPHE); + if (!lines.isEmpty()) { + File file = findMaxVersionFile(partialDependency.getGroupId(), pathToGems); + if (file != null) { + String sha1 = ChecksumUtils.calculateSHA1(file); + fillDependency(sha1, partialDependency, getVersionFromFileName(file.getName(), partialDependency.getGroupId()), gemLockFile, pathToGems, dependencyInfos); + } else { + logger.warn("Can't find version for {}", partialDependency.getGroupId()); + removeChildren(dependencyInfos, partialDependency); + } + } else { + logger.warn("Can't find version for {}", partialDependency.getGroupId()); + removeChildren(dependencyInfos, partialDependency); + } + } else { + String sha1 = getRubyDependenciesSha1(partialDependency.getGroupId(), version, pathToGems); + fillDependency(sha1, partialDependency, version, gemLockFile, pathToGems, dependencyInfos); + } + } catch (Exception e) { + logger.warn("Could not remove partial dependency {} with invalid version {}", partialDependency.getGroupId(), e.getMessage()); + logger.debug("stacktrace {}", e.getStackTrace()); + } + } + } catch (FileNotFoundException e) { + logger.warn("Could not find {} - {}", gemLockFile.getName(), e.getMessage()); + logger.debug("stacktrace {}", e.getStackTrace()); + } catch (Exception e) { + logger.warn("Could not parse {} - {}", gemLockFile.getName(), e.getMessage()); + logger.debug("stacktrace {}", e.getStackTrace()); + } finally { + try { + bufferedReader.close(); + fileReader.close(); + } catch (IOException e) { + logger.warn("Can't close {} - {}", gemLockFile.getName(), e.getMessage()); + logger.debug("stacktrace {}", e.getStackTrace()); + } + } + } + + + /** + * Removes dependencies tree cycles from dependencies trees. + * Dependency cycle is a dependency that one (or more) of its grand-children or grand-parents is itself. + * This method use Recursive DFS - https://www.geeksforgeeks.org/detect-cycle-in-a-graph/ + * + * @param dependenciesInfo + */ + private void removeDependencyCycle(List dependenciesInfo) { + HashSet recursiveDependecies = new HashSet<>(); + + Iterator parentDependenciesIterator = dependenciesInfo.iterator(); + + while (parentDependenciesIterator.hasNext()) { + recursiveDependecies.clear(); + removeDependencyCycle(parentDependenciesIterator.next(), recursiveDependecies); + } + } + + private boolean removeDependencyCycle(DependencyInfo dependencyInfo, HashSet recursiveDependecies) { + if (recursiveDependecies.contains(dependencyInfo)) { + logger.debug("Dependency Cycle: {} ", dependencyInfo.getArtifactId()); + return true; + } + + recursiveDependecies.add(dependencyInfo); + + List childrenToRemove = new ArrayList<>(); + dependencyInfo.getChildren().forEach(d -> { + if (removeDependencyCycle(d, recursiveDependecies)) { + logger.debug("Dependency Cycle: Remove {} from Parent {}", d.getArtifactId(), dependencyInfo.getArtifactId()); + childrenToRemove.add(d); + } + }); + + dependencyInfo.getChildren().removeAll(childrenToRemove); + recursiveDependecies.remove(dependencyInfo); + return false; + } + + private void fillDependency(String sha1, DependencyInfo partialDependency, String version, File gemLockFile, String pathToGems, List dependencyInfos) { + if (sha1 == null) { + logger.warn("Can't find gem file for {}-{}", partialDependency.getGroupId(), version); + removeChildren(dependencyInfos, partialDependency); + } else { + partialDependency.setSha1(sha1); + setDependencyInfoProperties(partialDependency, partialDependency.getGroupId(), version, gemLockFile, pathToGems); + } + } + + /* + * The result is a negative integer if str1 is _numerically_ less than str2. + * The result is a positive integer if str1 is _numerically_ greater than str2. + * The result is zero if the strings are _numerically_ equal. + * + */ + public static int versionCompare(String str1, String str2) { + String[] vals1 = str1.split("\\."); + String[] vals2 = str2.split("\\."); + int i = 0; + // set index to first non-equal ordinal or length of shortest version string + while (i < vals1.length && i < vals2.length && vals1[i].equals(vals2[i])) { + i++; + } + // compare first non-equal ordinal number + if (i < vals1.length && i < vals2.length) { + int diff = Integer.valueOf(vals1[i]).compareTo(Integer.valueOf(vals2[i])); + return Integer.signum(diff); + } + // the strings are equal or one string is a substring of the other + // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" + return Integer.signum(vals1.length - vals2.length); + } + + private void removeChildren(Collection dependencyInfos, DependencyInfo child) { + Iterator iterator = dependencyInfos.iterator(); + while (iterator.hasNext()) { + DependencyInfo dependencyInfo = iterator.next(); + if (dependencyInfo.getChildren().size() > 0) { + removeChildren(dependencyInfo.getChildren(), child); + } else if (dependencyInfo.equals(child)) { + iterator.remove(); + } + } + } + + private void setDependencyInfoProperties(DependencyInfo dependencyInfo, String name, String version, File gemLockFile, String pathToGems) { + dependencyInfo.setArtifactId(name + Constants.DASH + version + Constants.DOT + GEM); + dependencyInfo.setVersion(version); + dependencyInfo.setDependencyType(DependencyType.RUBY); + dependencyInfo.setDependencyFile(gemLockFile.getPath()); + dependencyInfo.setSystemPath(pathToGems + fileSeparator + name + Constants.DASH + version + Constants.DOT + GEM); + dependencyInfo.setFilename(name + Constants.DASH + version + Constants.DOT + GEM); + } + + // Ruby's cache is inside the installation folder. path can be found by running command 'gem environment gemdir' + private String findPathToGems() throws FileNotFoundException { + String[] commandParams = cli.getCommandParams(GEM, ENVIRONMENT); + List lines = cli.runCmd(rootDirectory, commandParams); + String path = null; + if (!lines.isEmpty()) { + path = lines.get(0) + fileSeparator + CACHE; + if (new File(path).isDirectory() == false) { + throw new FileNotFoundException(); + } + } + return path; + } + + private String getRubyDependenciesSha1(String name, String version, String pathToGems) throws IOException { + String sha1 = null; + File file = new File(pathToGems + fileSeparator + name + Constants.DASH + version + Constants.DOT + GEM); + if (file.isFile()) { + sha1 = ChecksumUtils.calculateSHA1(file); + } else { + file = installMissingGem(name, version, file); + if (file != null) { + sha1 = ChecksumUtils.calculateSHA1(file); + } + } + return sha1; + } + + private File installMissingGem(String name, String version, File file) { + if (installMissingGems) { + logger.info("installing gem file for {}-{}", name, version); + if (version.toLowerCase().contains(MINGW)) { + version = version.substring(0, version.indexOf(Constants.DASH)); + } + List lines = installGem(name, version); + if (file.isFile()) { + return file; + } + if (!lines.isEmpty()) { + List errors = lines.stream().filter(line -> line.startsWith(ERROR)).collect(Collectors.toList()); + if (errors.size() > 0) { + return null; + } + /* there are some cases where a gem is installed successfully, but with a slightly different name, e.g. + 'pg -v 0.21.0' becomes 'pg-0.21.0-x64-mingw32' + for those cases, this piece of code extracts the updated version and return the downloaded file + */ + try { + String installed = lines.stream().filter(line -> line.startsWith("Successfully installed") && line.contains(name)).findFirst().orElse(Constants.EMPTY_STRING); + String gem = installed.split(Constants.WHITESPACE)[2]; + File newFile = new File(file.getParent() + fileSeparator + gem + Constants.DOT + GEM); + if (newFile.isFile()) { + return newFile; + } + } catch (IndexOutOfBoundsException e) { + logger.warn("failed installing gem file for {}-{}", name, version); + logger.debug("stacktrace {}", e.getStackTrace()); + } + } + } + return null; + } + + private List installGem(String name, String version) { + String param = Constants.INSTALL.concat(Constants.WHITESPACE + name + Constants.WHITESPACE + V + Constants.WHITESPACE + version); + String[] commandParams = cli.getCommandParams(GEM, param); + return cli.runCmd(rootDirectory, commandParams); + } + + // there are cases where a dependency appears in the Gemfile.lock only as a child. + // in such cases, look for the relevant gem file in the cache with the highest version + private String findGemVersion(String gemName, String pathToGems) { + String version = null; + File maxVersionFile = findMaxVersionFile(gemName, pathToGems); + if (maxVersionFile != null) { + String fileName = maxVersionFile.getName(); + version = getVersionFromFileName(fileName, gemName); + } + return version; + } + + private String getVersionFromFileName(String fileName, String gemName) { + return fileName.substring(gemName.length() + 1, fileName.lastIndexOf(Constants.DOT)); + } + + private File findMaxVersionFile(String gemName, String pathToGems) { + File gemsFolder = new File(pathToGems); + File[] files = gemsFolder.listFiles(new GemFileNameFilter(gemName)); + if (files.length > 0) { + Arrays.sort(files, Collections.reverseOrder()); + return files[0]; + } + return null; + } +} + +class GemFileNameFilter implements FilenameFilter { + + private String fileName; + + public GemFileNameFilter(String name) { + fileName = name; + } + + @Override + public boolean accept(File dir, String name) { + if (name.toLowerCase().startsWith(fileName) && name.endsWith(Constants.DOT + GEM)) { + int indx = name.toLowerCase().indexOf(fileName.toLowerCase()) + fileName.length() + 1; // index of first char after fileName + "-" + return Character.isDigit(name.charAt(indx)); + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/sbt/IvyReport.java b/src/main/java/org/whitesource/agent/dependency/resolver/sbt/IvyReport.java new file mode 100644 index 0000000..40b5387 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/sbt/IvyReport.java @@ -0,0 +1,156 @@ +package org.whitesource.agent.dependency.resolver.sbt; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.List; + +@Root(strict=false) +public class IvyReport { + + @Element + private Info info; + + @ElementList + private List dependencies; + + /* --- Getters --- */ + + public List getDependencies() { + return dependencies; + } + + public Info getInfo() { + return info; + } +} + +@Root(strict = false) +class Info { + + @Attribute + private String organisation; + + @Attribute + private String module; + + @Attribute + private String revision; + + /* --- Getters --- */ + + public String getGroupId() { + return organisation; + } + + public String getArtifactId() { + return module; + } + + public String getVersion() { + return revision; + } +} + +class Module { + + @Attribute + private String organisation; + + @Attribute + private String name; + + @ElementList(inline = true) + private List revisionsList; + + /* --- Getters --- */ + + public String getGroupId() { + return organisation; + } + + public String getArtifactId() { + return name; + } + + public List getRevisions() { + return revisionsList; + } +} + +@Root(strict=false) +class Revision{ + + @Attribute + private String name; + + @Attribute + private int position; + + @ElementList(inline = true) + private List callerList; + + @ElementList + private List artifacts; + + /* --- Getters --- */ + + public String getVersion() { + return name; + } + + // dependencies with multiple versions, only the latest is used. the others have property 'position=-1" + public boolean isIgnored(){ + return position == -1; + } + + public List getParentsList() { + return callerList; + } + + public List getArtifacts() { + return artifacts; + } +} + +@Root(strict=false) +class Caller{ + + @Attribute + private String organisation; + + @Attribute + private String name; + + @Attribute + private String callerrev; + + /* --- Getters --- */ + + public String getGroupId() { + return organisation; + } + + public String getArtifactId() { + return name; + } + + public String getVersion() { + return callerrev; + } +} + +@Root(strict=false) +class Artifact{ + + @Attribute + private String location; + + /* --- Getters --- */ + + public String getPathToJar() { + return location; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtBomParser.java b/src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtBomParser.java new file mode 100644 index 0000000..55aadfa --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtBomParser.java @@ -0,0 +1,33 @@ +package org.whitesource.agent.dependency.resolver.sbt; + +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.core.Persister; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.dependency.resolver.BomFile; +import org.whitesource.agent.dependency.resolver.IBomParser; +import org.whitesource.agent.dependency.resolver.maven.MavenPomParser; + +import java.io.File; + +public class SbtBomParser implements IBomParser { + private final Logger logger = LoggerFactory.getLogger(SbtBomParser.class); + @Override + public BomFile parseBomFile(String bomPath) { + File bomFile = new File(bomPath); + if (bomFile.isFile()){ + Serializer serializer = new Persister(); + try { + IvyReport ivyReport = serializer.read(IvyReport.class, bomFile); + String groupId = ivyReport.getInfo().getGroupId(); + String artifactId = ivyReport.getInfo().getArtifactId(); + String version = ivyReport.getInfo().getVersion(); + return new BomFile(groupId, artifactId, version, bomPath); + } catch (Exception e) { + logger.warn("Couldn't parse {}, {}", bomPath, e.getMessage()); + logger.debug("stacktrace {}", e.getStackTrace()); + } + } + return null; + } +} diff --git a/src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtDependencyResolver.java b/src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtDependencyResolver.java new file mode 100644 index 0000000..2c76e33 --- /dev/null +++ b/src/main/java/org/whitesource/agent/dependency/resolver/sbt/SbtDependencyResolver.java @@ -0,0 +1,344 @@ +package org.whitesource.agent.dependency.resolver.sbt; + +import org.apache.commons.lang.StringUtils; +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.core.Persister; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.api.model.DependencyType; +import org.whitesource.agent.dependency.resolver.AbstractDependencyResolver; +import org.whitesource.agent.dependency.resolver.DependencyCollector; +import org.whitesource.agent.dependency.resolver.ResolutionResult; +import org.whitesource.agent.hash.ChecksumUtils; +import org.whitesource.agent.utils.Cli; +import org.whitesource.agent.utils.FilesScanner; +import org.whitesource.agent.utils.FilesUtils; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class SbtDependencyResolver extends AbstractDependencyResolver { + + /* --- Static members --- */ + + private static final String BUILD_SBT = "build.sbt"; + private static final String SCALA = "scala"; + private static final String SBT = "sbt"; + private static final String SCALA_EXTENSION = Constants.DOT + SCALA; + private static final List SCALA_SCRIPT_EXTENSION = Arrays.asList(SCALA_EXTENSION,Constants.DOT + SBT); + private static final String COMPILE = "compile"; + private static final String TARGET = "target"; + private static final String RESOLUTION_CACHE = "resolution-cache"; + private static final String REPORTS = "reports"; + private static final String SUCCESS = "success"; + private static final String PROJECT = "project"; + private static final String COMPILE_XML = "-compile.xml"; + private static final String SBT_TARGET_FOLDER = "sbt.targetFolder"; + private static final Pattern linuxPattern = Pattern.compile("\\/.*\\/target"); + private static final String windowsPattern = ".*\\s"; + + /* --- Private Members --- */ + + private boolean sbtAggregateModules; + private boolean ignoreSourceFiles; + private boolean sbtRunPreStep; + private String sbtTargetFolder; + private String[] includes = {"**" + fileSeparator + TARGET + fileSeparator + "**" + fileSeparator + Constants.EMPTY_STRING + RESOLUTION_CACHE + fileSeparator + REPORTS + fileSeparator + "*" + COMPILE_XML}; + private String[] excludes = {"**" + fileSeparator + PROJECT + fileSeparator + "**"}; + private final Logger logger = LoggerFactory.getLogger(SbtDependencyResolver.class); + + /* --- Constructors --- */ + + public SbtDependencyResolver(boolean sbtAggregateModules, boolean ignoreSourceFiles, boolean sbtRunPreStep, String sbtTargetFolder) { + this.sbtAggregateModules = sbtAggregateModules; + this.ignoreSourceFiles = ignoreSourceFiles; + this.bomParser = new SbtBomParser(); + this.sbtRunPreStep = sbtRunPreStep; + this.sbtTargetFolder = sbtTargetFolder; + } + + /* --- Overridden methods --- */ + + @Override + protected ResolutionResult resolveDependencies(String projectFolder, String topLevelFolder, Set bomFiles) { + Collection projects = new ArrayList<>(); + List xmlFiles = new LinkedList<>(); + + // run sbt compile if the user turn on the sbt.runPreStep flag + if (sbtRunPreStep) { + runPreStep(topLevelFolder); + } + + // check if sbt.targetFolder is not blank. + // if not the system trying to search for compile.xml files under the the specific location + // if yes the system trying to search for compile.xml files under the root of the project + if (StringUtils.isNotBlank(sbtTargetFolder)) { + Path path = Paths.get(sbtTargetFolder); + if (Files.exists(path)) { + xmlFiles = findXmlReport(sbtTargetFolder, xmlFiles, new String[]{Constants.PATTERN + COMPILE_XML}, excludes); + } else { + logger.warn("The target folder path {} doesn't exist", sbtTargetFolder); + } + } else { + Collection targetFolders = findTargetFolders(topLevelFolder); + if (!targetFolders.isEmpty()) { + for (String targetPath : targetFolders) { + xmlFiles = findXmlReport(targetPath, xmlFiles, new String[]{Constants.PATTERN + COMPILE_XML}, excludes); + } + } else { + logger.debug("Didn't find any target folder in {}", topLevelFolder); + } + } + + // if the system didn't find compile.xml files and the user didn't turn on the sbt.runPreStepFlag + // the system print warning message to the user and ask him to turn on the flag. + if (xmlFiles.isEmpty() && !sbtRunPreStep) { + logger.warn("Didn't find compile.xml please try to turn on the flag {}", SBT_TARGET_FOLDER); + } + for (File xmlFile : xmlFiles) { + projects.add(parseXmlReport(xmlFile)); + } + Set excludes = new HashSet<>(); + Map projectInfoPathMap = projects.stream().collect(Collectors.toMap(projectInfo -> projectInfo, projectInfo -> { + if (ignoreSourceFiles) { + excludes.addAll(normalizeLocalPath(projectFolder, topLevelFolder, extensionPattern(SCALA_SCRIPT_EXTENSION), null)); + } + return Paths.get(topLevelFolder); + })); + + ResolutionResult resolutionResult; + if (!sbtAggregateModules) { + resolutionResult = new ResolutionResult(projectInfoPathMap, excludes, getDependencyType(), topLevelFolder); + } else { + resolutionResult = new ResolutionResult(projectInfoPathMap.keySet().stream() + .flatMap(project -> project.getDependencies().stream()).collect(Collectors.toList()), excludes, getDependencyType(), topLevelFolder); + } + return resolutionResult; + } + + @Override + protected Collection getExcludes() { + Set excludes = new HashSet<>(); + excludes.add(Constants.PATTERN + SCALA_EXTENSION); + excludes.add("**" + fileSeparator + PROJECT + fileSeparator + "**"); + return excludes; + } + + @Override + public Collection getSourceFileExtensions() { + return SCALA_SCRIPT_EXTENSION; + } + + @Override + protected DependencyType getDependencyType() { + return DependencyType.MAVEN; // TEMP - we should add SBT + } + + @Override + protected String getDependencyTypeName() { + return SBT.toUpperCase(); + } + + @Override + public String[] getBomPattern() { + return new String[]{Constants.PATTERN + BUILD_SBT}; + } + + @Override + public Collection getManifestFiles(){ + return Arrays.asList(BUILD_SBT); + } + + @Override + protected Collection getLanguageExcludes() { + return null; + } + + /* --- Private methods --- */ + + /* looking for an xml file ending with '-compile.xml', which should be either in 'target/scala-{version number}/resolution-cache/reports' + or 'target/scala-{version number}/sbt-{version-number}/resolution-cache/reports'. + There are some cases where 2 files inside that folder end with '-compile.xml'. in such case, the way to find the relevant is if its name + contains the scala-version (which is part of the scala folder name), and that's relevant only if the xml file isn't inside 'sbt-{}' folder. + */ + private List findXmlReport(String folderPath, List files, String[] includes, String[] excludes) { + FilesScanner filesScanner = new FilesScanner(); + logger.debug("Trying to find *" + COMPILE_XML + " file under target folder in {}", folderPath); + String[] directoryContent = filesScanner.getDirectoryContent(folderPath, includes, excludes, false, false); + for (String filePath : directoryContent) { + boolean add = true; + File reportFile = new File(folderPath + fileSeparator + filePath); + for (File file : files) { + if (file.getParent().equals(reportFile.getParent())) { + add = false; + if (reportFile.getName().length() < file.getName().length()) { + files.remove(file); + files.add(reportFile); + break; + } + } + } + if (add) { + files.add(reportFile); + } + } + return files; + } + + // creating the xml report using 'sbt "compile"' command + private void runPreStep(String folderPath) { + Cli cli = new Cli(); + boolean success = false; + List compileOutput = cli.runCmd(folderPath, cli.getCommandParams(SBT, COMPILE)); + if (!compileOutput.isEmpty()) { + if (compileOutput.get(compileOutput.size() - 1).contains(SUCCESS)) { + success = true; + } + } + if (!success) { + logger.warn("Can't run '{} {}'", SBT, COMPILE); + } + } + + // Trying to get all the paths of target folders + private Collection findTargetFolders(String folderPath) { + logger.debug("Scanning target folder {}", folderPath); + Cli cli = new Cli(); + List lines; + List targetFolders = new LinkedList<>(); + lines = cli.runCmd(folderPath, cli.getCommandParams(SBT, TARGET)); + if (lines != null && !lines.isEmpty()) { + for (String line : lines) { + if (DependencyCollector.isWindows()) { + if (line.endsWith(TARGET) && line.contains(fileSeparator)) { + String[] split = line.split(windowsPattern); + targetFolders.add(split[1]); + } + } else { + if (line.contains(TARGET) && line.contains(fileSeparator)) { + Matcher matcher = linuxPattern.matcher(line); + if (matcher.find()) { + targetFolders.add(matcher.group(0)); + } + } + } + } + for (int i = 0; i < targetFolders.size(); i++) { + String targetFolder = targetFolders.get(i); + Path path = Paths.get(targetFolder); + if (!Files.exists(path)) { + targetFolders.remove(targetFolder); + logger.warn("The target folder {} path doesn't exist", sbtTargetFolder); + } + } + } + return targetFolders; + } + + private AgentProjectInfo parseXmlReport(File xmlReportFile) { + AgentProjectInfo agentProjectInfo = new AgentProjectInfo(); + Serializer serializer = new Persister(); + Map parentsMap = new HashMap<>(); + Map> childrenMap = new HashMap<>(); + try { + IvyReport ivyReport = serializer.read(IvyReport.class, xmlReportFile); + // using these properties to identify root dependencies (having the project's root as their parent) + String projectGroupId = ivyReport.getInfo().getGroupId(); + String projectArtifactId = ivyReport.getInfo().getArtifactId(); + String projectVersion = ivyReport.getInfo().getVersion(); + agentProjectInfo.setCoordinates(new Coordinates(projectGroupId, projectArtifactId, projectVersion)); + for (Module dependency : ivyReport.getDependencies()) { + String groupId = dependency.getGroupId(); + String artifactId = dependency.getArtifactId(); + for (Revision revision : dependency.getRevisions()) { + // making sure this dependency's version is used (and not over-written by a newer version) + if (!revision.isIgnored()) { + String version = revision.getVersion(); + //Artifact artifact = revision.getArtifacts().get(0); // resolving path to jar file + if (revision.getArtifacts().size() > 0 && revision.getArtifacts().get(0) != null) { + File jarFile = new File(revision.getArtifacts().get(0).getPathToJar()); + if (jarFile.isFile()) { + String sha1 = ChecksumUtils.calculateSHA1(jarFile); + if (sha1 != null) { + DependencyInfo dependencyInfo = new DependencyInfo(groupId, artifactId, version); + dependencyInfo.setSha1(sha1); + dependencyInfo.setDependencyType(DependencyType.MAVEN); + dependencyInfo.setFilename(jarFile.getName()); + dependencyInfo.setSystemPath(jarFile.getPath()); + + String extension = FilesUtils.getFileExtension(jarFile.getName()); + dependencyInfo.setType(extension); + + String dependencyName = groupId + Constants.COLON + artifactId + Constants.COLON + version; + parentsMap.put(dependencyName, dependencyInfo); + for (Caller parent : revision.getParentsList()) { + String parentGroupId = parent.getGroupId(); + String parentArtifactId = parent.getArtifactId(); + // if this dependency's parent is the root - no need to add is as a child... + if (parentGroupId.equals(projectGroupId) == false && parentArtifactId.equals(projectArtifactId) == false) { + String parentVersion = parent.getVersion(); + String parentName = parentGroupId + Constants.COLON + parentArtifactId + Constants.COLON + parentVersion; + DependencyInfo parentDependencyInfo = parentsMap.get(parentName); + if (parentDependencyInfo != null) { // the parent was already created - add it as a child + parentDependencyInfo.getChildren().add(dependencyInfo); + } else { // add this dependency to the children map + if (childrenMap.get(dependencyName) == null) { + childrenMap.put(dependencyName, new ArrayList<>()); + } + childrenMap.get(dependencyName).add(parentName); + } + } else { //... add it directly to the dependency info list + agentProjectInfo.getDependencies().add(dependencyInfo); + } + } + } else { + logger.warn("Could not find SHA1 for {}-{}-{}", groupId, revision.getArtifacts().get(0), version); + } + } else { + logger.warn("Could not find jar file {}", jarFile.getPath()); + } + } else { + logger.warn("Could not find artifact ID for {}-{}", groupId, version); + } + } + } + } + // building dependencies tree + for (String child : childrenMap.keySet()) { + List parents = childrenMap.get(child); + for (String parent : parents) { + if (!isDescendant(parentsMap.get(child), parentsMap.get(parent))) { + parentsMap.get(parent).getChildren().add(parentsMap.get(child)); + } + } + } + } catch (Exception e) { + logger.warn("Could not read {}: {}", xmlReportFile.getPath(), e.getMessage()); + logger.debug("stacktrace {}", e.getStackTrace()); + } + + return agentProjectInfo; + } + + // preventing circular dependencies by making sure the dependency is not a descendant of its own + private boolean isDescendant(DependencyInfo ancestor, DependencyInfo descendant) { + for (DependencyInfo child : ancestor.getChildren()) { + if (child.equals(descendant)) { + return true; + } + return isDescendant(child, descendant); + } + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/utils/AddDependencyFileRecursionHelper.java b/src/main/java/org/whitesource/agent/utils/AddDependencyFileRecursionHelper.java new file mode 100644 index 0000000..37f9751 --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/AddDependencyFileRecursionHelper.java @@ -0,0 +1,13 @@ +package org.whitesource.agent.utils; + +import org.whitesource.agent.api.model.DependencyInfo; + +import java.util.stream.Stream; + +public class AddDependencyFileRecursionHelper { + private AddDependencyFileRecursionHelper(){} + + public static Stream flatten(DependencyInfo dependencyInfo){ + return Stream.concat(Stream.of(dependencyInfo), dependencyInfo.getChildren().stream().flatMap(AddDependencyFileRecursionHelper::flatten)); + } +} diff --git a/src/main/java/org/whitesource/agent/utils/Cli.java b/src/main/java/org/whitesource/agent/utils/Cli.java new file mode 100644 index 0000000..5faaaf6 --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/Cli.java @@ -0,0 +1,59 @@ +package org.whitesource.agent.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.DependencyCollector; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +public class Cli { + private final Logger logger = LoggerFactory.getLogger(Cli.class); + + public List runCmd(String rootDirectory, String[] params){ + try { + CommandLineProcess commandLineProcess = new CommandLineProcess(rootDirectory, params); + List lines = commandLineProcess.executeProcess(); + if (!commandLineProcess.isErrorInProcess()) { + return lines; + } + } catch (IOException e) { + logger.warn("Error getting dependencies after running {} on {}, {}" , params , rootDirectory, e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + return new LinkedList<>(); + } + + public String[] getCommandParams(String command, String param){ + if (param.contains(Constants.WHITESPACE)){ + return getCommandParamsArray(command, param); + } + if (DependencyCollector.isWindows()) { + return new String[] {Constants.CMD, DependencyCollector.C_CHAR_WINDOWS, command, param}; + } + return new String[] {command, param}; + } + + private String[] getCommandParamsArray(String command, String param){ + String[] params = param.split(Constants.WHITESPACE); + String[] output; + if (DependencyCollector.isWindows()) { + output = new String[3 + params.length]; + output[0] = Constants.CMD; + output[1] = DependencyCollector.C_CHAR_WINDOWS; + output[2] = command; + for (int i = 0; i < params.length; i++){ + output[i + 3] = params[i]; + } + } else { + output = new String[1 + params.length]; + output[0] = command; + for (int i = 0; i < params.length; i++){ + output[i + 1] = params[i]; + } + } + return output; + } +} diff --git a/src/main/java/org/whitesource/agent/utils/CommandLineProcess.java b/src/main/java/org/whitesource/agent/utils/CommandLineProcess.java new file mode 100644 index 0000000..4393c2e --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/CommandLineProcess.java @@ -0,0 +1,267 @@ +package org.whitesource.agent.utils; + +import com.sun.jna.Native; +import com.sun.jna.platform.win32.Kernel32; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.DependencyCollector; + +import java.io.*; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.*; + +/** + * @author raz.nitzan + */ +public class CommandLineProcess { + + /* --- Members --- */ + + private String rootDirectory; + private String[] args; + private long timeoutReadLineSeconds; + private long timeoutProcessMinutes; + private boolean errorInProcess = false; + private Process processStart = null; + private File errorLog = new File(UniqueNamesGenerator.createUniqueName("error", ".log")); + + /* --- Statics Members --- */ + private static final long DEFAULT_TIMEOUT_READLINE_SECONDS = 300; + private static final long DEFAULT_TIMEOUT_PROCESS_MINUTES = 15; + private static final String WINDOWS_SEPARATOR = "\\"; + private final Logger logger = LoggerFactory.getLogger(org.whitesource.agent.utils.CommandLineProcess.class); + + public CommandLineProcess(String rootDirectory, String[] args) { + this.rootDirectory = rootDirectory; + this.args = args; + this.timeoutReadLineSeconds = DEFAULT_TIMEOUT_READLINE_SECONDS; + this.timeoutProcessMinutes = DEFAULT_TIMEOUT_PROCESS_MINUTES; + } + + public List executeProcess() throws IOException { + return executeProcess(true, false); + } + + private List executeProcess(boolean includeOutput, boolean includeErrorLines) throws IOException { + List linesOutput = new LinkedList<>(); + ProcessBuilder pb = new ProcessBuilder(args); + String osName = System.getProperty(Constants.OS_NAME); + if (osName.startsWith(Constants.WINDOWS)) { + rootDirectory = getShortPath(rootDirectory); + } + pb.directory(new File(rootDirectory)); + // redirect the error output to avoid output of npm ls by operating system + String redirectErrorOutput = DependencyCollector.isWindows() ? "nul" : "/dev/null"; + if (includeErrorLines) { + pb.redirectError(errorLog); + } else { + pb.redirectError(new File(redirectErrorOutput)); + } + if (!includeOutput || includeErrorLines) { + pb.redirectOutput(new File(redirectErrorOutput)); + } + if (!includeErrorLines) { + logger.debug("start execute command '{}' in '{}'", String.join(Constants.WHITESPACE, args), rootDirectory); + } + this.processStart = pb.start(); + if (includeOutput) { + InputStreamReader inputStreamReader; + BufferedReader reader; + ExecutorService executorService = Executors.newFixedThreadPool(1); + if (!includeErrorLines) { + inputStreamReader = new InputStreamReader(this.processStart.getInputStream()); + } else { + inputStreamReader = new InputStreamReader(this.processStart.getErrorStream()); + } + reader = new BufferedReader(inputStreamReader); + this.errorInProcess = readBlock(inputStreamReader, reader, executorService, linesOutput, includeErrorLines); + } + try { + this.processStart.waitFor(this.timeoutProcessMinutes, TimeUnit.MINUTES); + } catch (InterruptedException e) { + this.errorInProcess = true; + logger.error("'{}' was interrupted {}", args, e); + } + if (this.processStart.isAlive() && errorInProcess) { + logger.debug("error executing command destroying process"); + this.processStart.destroy(); + return linesOutput; + } + if (this.getExitStatus() != 0) { + logger.debug("error in execute command {}", this.getExitStatus()); + this.errorInProcess = true; + } + printErrors(); + return linesOutput; + } + + // using this technique to print to the log the Process's errors as it the easiest way i found to do so - + // ues a file to redirect the errors to, read from it and then delete it. + // if you find a better way - go ahead and replace it + private void printErrors(){ + if (errorLog.isFile()){ + FileReader fileReader; + try { + fileReader = new FileReader(errorLog); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String currLine; + while ((currLine = bufferedReader.readLine()) != null){ + logger.debug(currLine); + } + fileReader.close(); + } catch (Exception e) { + logger.warn("Error printing cmd command errors {} " , e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } finally { + try { + FileUtils.forceDelete(errorLog); + } catch (IOException e) { + logger.warn("Error closing cmd command errors file {} " , e.getMessage()); + logger.debug("Error: {}", e.getStackTrace()); + } + } + } + } + + //get windows short path + private String getShortPath(String rootPath) { + File file = new File(rootPath); + String lastPathAfterSeparator = null; + String shortPath = getWindowsShortPath(file.getAbsolutePath()); + if (StringUtils.isNotEmpty(shortPath)) { + return getWindowsShortPath(file.getAbsolutePath()); + } else { + while (StringUtils.isEmpty(getWindowsShortPath(file.getAbsolutePath()))) { + String filePath = file.getAbsolutePath(); + if (StringUtils.isNotEmpty(lastPathAfterSeparator)) { + lastPathAfterSeparator = file.getAbsolutePath().substring(filePath.lastIndexOf(WINDOWS_SEPARATOR), filePath.length()) + lastPathAfterSeparator; + } else { + lastPathAfterSeparator = file.getAbsolutePath().substring(filePath.lastIndexOf(WINDOWS_SEPARATOR), filePath.length()); + } + file = file.getParentFile(); + } + return getWindowsShortPath(file.getAbsolutePath()) + lastPathAfterSeparator; + } + } + + private String getWindowsShortPath(String path) { + if (path.length() >= 256){ + char[] result = new char[256]; + + //Call CKernel32 interface to execute GetShortPathNameA method + Kernel32.INSTANCE.GetShortPathName(path, result, result.length); + return Native.toString(result); + } + return path; + } + + private boolean readBlock(InputStreamReader inputStreamReader, BufferedReader reader, ExecutorService executorService, List lines, boolean includeErrorLines) { + boolean wasError = false; + boolean continueReadingLines = true; + try { + if (!includeErrorLines) { + logger.debug("trying to read lines using '{}'", commandArgsToString()); + } + int lineIndex = 1; + String line = Constants.EMPTY_STRING; + while (continueReadingLines && line != null) { + Future future = executorService.submit(new CommandLineProcess.ReadLineTask(reader)); + try { + line = future.get(this.timeoutReadLineSeconds, TimeUnit.SECONDS); + if (!includeErrorLines) { + if (StringUtils.isNotBlank(line)) { + logger.debug("Read line #{}: {}", lineIndex, line); + lines.add(line); + } else { + logger.debug("Finished reading {} lines", lineIndex - 1); + } + } else { + if (StringUtils.isNotBlank(line)) { + lines.add(line); + } + } + } catch (TimeoutException e) { + logger.debug("Received timeout when reading line #" + lineIndex, e.getStackTrace()); + continueReadingLines = false; + wasError = true; + } catch (Exception e) { + logger.debug("Error reading line #" + lineIndex, e.getStackTrace()); + continueReadingLines = false; + wasError = true; + } + lineIndex++; + } + } catch (Exception e) { + logger.error("error parsing output : {}", e.getStackTrace()); + } finally { + executorService.shutdown(); + IOUtils.closeQuietly(inputStreamReader); + IOUtils.closeQuietly(reader); + } + return wasError; + } + + private String commandArgsToString() { + StringBuilder result = new StringBuilder(Constants.EMPTY_STRING); + for (String arg : this.args) { + result.append(arg + Constants.WHITESPACE); + } + // delete last whitespace + result.deleteCharAt(result.length() - 1); + return result.toString(); + } + + public void executeProcessWithoutOutput() throws IOException { + executeProcess(false, false); + } + + public List executeProcessWithErrorOutput() throws IOException { + return executeProcess(false, true); + } + + public void setTimeoutReadLineSeconds(long timeoutReadLineSeconds) { + this.timeoutReadLineSeconds = timeoutReadLineSeconds; + } + + public void setTimeoutProcessMinutes(long timeoutProcessMinutes) { + this.timeoutProcessMinutes = timeoutProcessMinutes; + } + + public boolean isErrorInProcess() { + return this.errorInProcess; + } + + public int getExitStatus() { + if (processStart != null) { + return processStart.exitValue(); + } + return 0; + } + + /* --- Nested classes --- */ + + class ReadLineTask implements Callable { + + /* --- Members --- */ + + private final BufferedReader reader; + + /* --- Constructors --- */ + + ReadLineTask(BufferedReader reader) { + this.reader = reader; + } + + /* --- Overridden methods --- */ + + @Override + public String call() throws Exception { + return reader.readLine(); + } + } +} diff --git a/src/main/java/org/whitesource/agent/utils/FilesScanner.java b/src/main/java/org/whitesource/agent/utils/FilesScanner.java new file mode 100644 index 0000000..b4b6f1b --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/FilesScanner.java @@ -0,0 +1,155 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.utils; + +import org.apache.tools.ant.DirectoryScanner; +import org.slf4j.Logger; +import org.whitesource.agent.SingleFileScanner; +import org.whitesource.agent.dependency.resolver.ResolvedFolder; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author eugen.horovitz + */ +public class FilesScanner { + + /* --- Static members --- */ + + private Logger logger = LoggerFactory.getLogger(FilesScanner.class); + + /* --- Public methods --- */ + + public String[] getDirectoryContent(String scannerBaseDir, String[] includes, String[] excludes, boolean followSymlinks, boolean globCaseSensitive) { + return getDirectoryContent(scannerBaseDir, includes, excludes, followSymlinks, globCaseSensitive, false); + } + + // get the content of directory by includes, excludes, followSymlinks and globCaseSensitive, the scanDirectories property define if the scanner will scan to find directories + public String[] getDirectoryContent(String scannerBaseDir, String[] includes, String[] excludes, boolean followSymlinks, boolean globCaseSensitive, boolean scanDirectories) { + File file = new File(scannerBaseDir); + String[] fileNames; + if (file.exists() && file.isDirectory()) { + DirectoryScanner scanner = new DirectoryScanner(); + scanner.setBasedir(scannerBaseDir); + scanner.setIncludes(includes); + scanner.setExcludes(excludes); + scanner.setFollowSymlinks(followSymlinks); + scanner.setCaseSensitive(globCaseSensitive); + scanner.scan(); + if (!scanDirectories) { + fileNames = scanner.getIncludedFiles(); + } else { + fileNames = scanner.getIncludedDirectories(); + } + return fileNames; + } else { + logger.debug("{} is not a folder", scannerBaseDir); + return new String[0]; + } + } + + public Collection findTopFolders(Collection pathsToScan, String[] includesPattern, Collection excludes) { + Collection resolvedFolders = new ArrayList<>(); + // get folders containing bom files + Map pathToBomFilesMap = findAllFiles(pathsToScan, includesPattern, excludes); + + // resolve dependencies + pathToBomFilesMap.forEach((folder, bomFile) -> { + // get top folders with boms (the parent of each project) + Map> topFolders = getTopFoldersWithIncludedFiles(folder, bomFile); + resolvedFolders.add(new ResolvedFolder(folder, topFolders)); + }); + return resolvedFolders; + } + + /* --- Private methods --- */ + + private Map findAllFiles(Collection pathsToScan, String[] includesPattern, Collection excludes) { + Map pathToIncludedFilesMap = new HashMap<>(); + pathsToScan.stream().forEach(scanFolder -> { + String[] includedFiles = getDirectoryContent(new File(scanFolder).getPath(), includesPattern, + excludes.toArray(new String[excludes.size()]), false, false); + pathToIncludedFilesMap.put(new File(scanFolder).getAbsolutePath(), includedFiles); + }); + return pathToIncludedFilesMap; + } + + private Map> getTopFoldersWithIncludedFiles(String rootFolder, String[] includedFiles) { + // collect all full paths + List fullPaths = Arrays.stream(includedFiles) + .map(file -> Paths.get(new File(rootFolder).getAbsolutePath(), file).toString()) + .collect(Collectors.toList()); + + // get top folders + Map> foldersGroupedByLengthMap = fullPaths.stream() + .collect(Collectors.groupingBy(filename -> new File(filename).getParentFile().getParent())); + + // create result map with only the top folder and the corresponding bom files + Map> resultMap = new HashMap<>(); + + logger.debug("found folders:" + System.lineSeparator()); + foldersGroupedByLengthMap.keySet().forEach(folder -> logger.debug(folder)); + logger.debug(System.lineSeparator()); + + while (foldersGroupedByLengthMap.entrySet().size() > 0) { + + String shortestFolder = foldersGroupedByLengthMap.keySet().stream().min(Comparator.comparingInt(String::length)).get(); + List foundShortestFolder = foldersGroupedByLengthMap.get(shortestFolder); + foldersGroupedByLengthMap.remove(shortestFolder); + + List topFolders = foundShortestFolder.stream() + .map(file -> new File(file).getParent()).collect(Collectors.toList()); + + topFolders.forEach(folder -> { + resultMap.put(folder, fullPaths.stream().filter(fileName -> fileName.contains(folder)).collect(Collectors.toSet())); + + // remove from list folders that are children of the one found so they will not be calculated twice + foldersGroupedByLengthMap.entrySet().removeIf(otherFolder -> { + Path otherFolderPath = Paths.get(otherFolder.getKey()); + Path folderPath = Paths.get(folder); + boolean shouldRemove = false; + try { + shouldRemove = otherFolderPath.toFile().getCanonicalPath().startsWith(folderPath.toFile().getCanonicalPath()); + } catch (Exception e) { + logger.debug("could not get file path " + otherFolderPath + folderPath, e.getStackTrace()); + logger.warn("could not get file path " + otherFolderPath + folderPath, e.getMessage()); + } + logger.debug(String.join(";", otherFolder.getKey(), folder, Boolean.toString(shouldRemove))); + if (shouldRemove) { + logger.debug("---> removed: " + otherFolder.getKey()); + return true; + } + return false; + }); + }); + } + logger.debug(System.lineSeparator()); + return resultMap; + } + + public boolean isIncluded(File file, String[] includes, String[] excludes, boolean followSymlinks, boolean globCaseSensitive) { + SingleFileScanner scanner = new SingleFileScanner(); + scanner.setIncludes(includes); + scanner.setExcludes(excludes); + scanner.setFollowSymlinks(followSymlinks); + scanner.setCaseSensitive(globCaseSensitive); + return scanner.isIncluded(file); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/utils/FilesUtils.java b/src/main/java/org/whitesource/agent/utils/FilesUtils.java new file mode 100644 index 0000000..e9e869f --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/FilesUtils.java @@ -0,0 +1,155 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.utils; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author eugen.horovitz + */ +public class FilesUtils { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(FilesUtils.class); + private final String JAVA_TEMP_DIR = System.getProperty("java.io.tmpdir"); + + + public String createTmpFolder(boolean addCharToEndOfUrl, String nameOfFolder) { + String result = getTempDirPackages(addCharToEndOfUrl, nameOfFolder); + try { + FileUtils.forceMkdir(new File(result)); + } catch (IOException e) { + logger.warn("Failed to create temp folder : " + e.getMessage()); + result = null; + } + return result; + } + + private String getTempDirPackages(boolean addCharToEndOfUrl, String nameOfFolder) { + String creationDate = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + String tempFolder = JAVA_TEMP_DIR.endsWith(File.separator) ? JAVA_TEMP_DIR + nameOfFolder + File.separator + creationDate : + JAVA_TEMP_DIR + File.separator + nameOfFolder + File.separator + creationDate; + if (addCharToEndOfUrl) { + tempFolder = tempFolder + "1"; + } + + return tempFolder; + } + + public List getSubDirectories(String directory, String[] includes, String[] excludesExtended, boolean followSymlinks, boolean globCaseSensitive) { + String[] files; + FilesScanner filesScanner = new FilesScanner(); + try { + files = filesScanner.getDirectoryContent(directory, includes, excludesExtended, followSymlinks, globCaseSensitive,true); + } catch (Exception ex) { + logger.info("Error getting sub directories from: " + directory, ex.getMessage()); + files = new String[0]; + } + return Arrays.stream(files).map(subDir -> Paths.get(directory, subDir)).collect(Collectors.toList()); + } + + public Map> fillFilesMap(Collection pathsToScan, String[] includes, String[] excludesExtended, + boolean followSymlinks, boolean globCaseSensitive) { + Map> fileMap = new HashMap<>(); + for (String scannerBaseDir : pathsToScan) { + File file = new File(scannerBaseDir); + logger.debug("Scanning {}", file.getAbsolutePath()); + if (file.exists()) { + FilesScanner filesScanner = new FilesScanner(); + if (file.isDirectory()) { + File basedir = new File(scannerBaseDir); + String[] fileNames = filesScanner.getDirectoryContent(scannerBaseDir, includes, excludesExtended, followSymlinks, globCaseSensitive); + // convert array to list (don't use Arrays.asList, might be added to later) + List fileNameList = Arrays.stream(fileNames).collect(Collectors.toList()); + fileMap.put(basedir, fileNameList); + } else { + // handle single file + boolean included = filesScanner.isIncluded(file, includes, excludesExtended, followSymlinks, globCaseSensitive); + if (included) { + Collection files = fileMap.get(file.getParentFile()); + if (files == null) { + files = new ArrayList<>(); + } + files.add(file.getName()); + fileMap.put(file.getParentFile(), files); + } + } + } else { + logger.info(MessageFormat.format("File {0} doesn\'t exist", scannerBaseDir)); + } + } + return fileMap; + } + + /* --- Static methods --- */ + + public static void deleteDirectory(File directory) { + if (directory != null) { + try { + FileUtils.forceDelete(directory); + } catch (IOException e) { + // do nothing + } + } + } + + public static String getFileExtension(String fileName) { + if(fileName == null) fileName = Constants.EMPTY_STRING; + String extension = Constants.EMPTY_STRING; + int i = fileName.lastIndexOf(Constants.DOT); + if (i > 0 && i < fileName.length()-2) { + extension = fileName.substring(i+1); + } + return extension; + } + + public static String removeTempFiles(String rootDirectory, long creationTime) { + String errors = ""; + FileTime fileCreationTime = FileTime.fromMillis(creationTime); + File directory = new File(rootDirectory); + File[] fList = directory.listFiles(); + if (fList != null) { + for (File file : fList) { + try { + BasicFileAttributes fileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class); + if (fileAttributes.creationTime().compareTo(fileCreationTime) > 0){ + FileUtils.forceDelete(file); + } else if (file.isDirectory()) { + errors = errors.concat(removeTempFiles(file.getPath(), creationTime)); + } + } catch (IOException e) { + errors = errors.concat("can't remove " + file.getPath() + ": " + e.getMessage() + '\n'); + } + } + } + return errors; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/agent/utils/LogContext.java b/src/main/java/org/whitesource/agent/utils/LogContext.java new file mode 100644 index 0000000..42639fe --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/LogContext.java @@ -0,0 +1,68 @@ +package org.whitesource.agent.utils; + +import org.whitesource.agent.Constants; + +import java.io.Serializable; +import java.util.Objects; +import java.util.Random; +import java.util.UUID; + +/** + * Created by anna.rozin + * Don't delete this class - it is being used by outside code + */ +public class LogContext implements Serializable { + + /* --- Static members --- */ + + private static final long serialVersionUID = 4342818989304453815L; + + protected static final String CONTEXT_FORMAT = "CTX=%s"; + + /* --- Members --- */ + + private String contextId; + + /* --- Constructors --- */ + + public LogContext(){ + contextId = new Random().nextInt(4) + UUID.randomUUID().toString().replace(Constants.DASH, Constants.EMPTY_STRING); + } + + /* --- Public methods --- */ + + public String getExtraContextString() { + return Constants.EMPTY_STRING; + } + + /* --- Overridden methods --- */ + + @Override + public String toString() { + return String.format(CONTEXT_FORMAT, contextId); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LogContext)) return false; + LogContext that = (LogContext) o; + return Objects.equals(contextId, that.contextId); + } + + @Override + public int hashCode() { + return Objects.hash(contextId); + } + + /* --- Getters / Setters --- */ + + public String getContextId() { + return contextId; + } + + public void setContextId(String contextId) { + this.contextId = contextId; + } + +} diff --git a/src/main/java/org/whitesource/agent/utils/LoggerFS.java b/src/main/java/org/whitesource/agent/utils/LoggerFS.java new file mode 100644 index 0000000..9c9158c --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/LoggerFS.java @@ -0,0 +1,360 @@ +package org.whitesource.agent.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; + +/** + * @author raz.nitzan + */ +public class LoggerFS implements Logger { + + /* --- Static Members --- */ + + private static final String OPENING_BRACKET = "["; + private static final String CLOSING_BRACKET = "] "; + private static final String CTX = "[CTX="; + + /* --- Members --- */ + + private final org.slf4j.Logger logger; + private final String contextId; + + /* --- Constructors --- */ + + public LoggerFS(Class clazz){ + this.logger = LoggerFactory.getLogger(clazz); + this.contextId = null; + } + + public LoggerFS(Class clazz, String contextId){ + this.logger = LoggerFactory.getLogger(clazz); + this.contextId = contextId; + } + + public LoggerFS(String name) { + this.logger = LoggerFactory.getLogger(name); + this.contextId = null; + } + + public LoggerFS(String name, String contextId) { + this.logger = LoggerFactory.getLogger(name); + this.contextId = contextId; + } + + @Override + public void info(String msg) { + this.logger.info(msgWithContextId(msg)); + } + + @Override + public void info(String msg, Object arg) { + this.logger.info(msgWithContextId(msg), arg); + } + + @Override + public void info(String msg, Object o, Object o1) { + this.logger.info(msgWithContextId(msg), o, o1); + } + + @Override + public void info(String format, Object... args) { + this.logger.info(msgWithContextId(format), args); + } + + @Override + public void info(String msg, Throwable t) { + this.logger.info(msgWithContextId(msg), t); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return this.isInfoEnabled(marker); + } + + @Override + public void info(Marker marker, String s) { + this.logger.info(marker, msgWithContextId(s)); + } + + @Override + public void info(Marker marker, String s, Object o) { + this.logger.info(marker, msgWithContextId(s), o); + } + + @Override + public void info(Marker marker, String s, Object o, Object o1) { + this.logger.info(marker, msgWithContextId(s), o, o1); + } + + @Override + public void info(Marker marker, String s, Object... objects) { + this.logger.info(marker, msgWithContextId(s), objects); + } + + @Override + public void info(Marker marker, String s, Throwable throwable) { + this.logger.info(marker, msgWithContextId(s), throwable); + } + + @Override + public void debug(String msg) { + this.logger.debug(msgWithContextId(msg)); + } + + @Override + public void debug(String msg, Object arg) { + this.logger.debug(msgWithContextId(msg), arg); + } + + @Override + public void debug(String s, Object o, Object o1) { + this.logger.debug(msgWithContextId(s), o, o1); + } + + @Override + public void debug(String format, Object... args) { + this.logger.debug(msgWithContextId(format), args); + } + + @Override + public void debug(String msg, Throwable t) { + this.logger.debug(msgWithContextId(msg), t); + } + + @Override + public void warn(String msg) { + this.logger.warn(msgWithContextId(msg)); + } + + @Override + public void warn(String msg, Object arg) { + this.logger.warn(msgWithContextId(msg), arg); + } + + @Override + public void warn(String format, Object... args) { + this.logger.warn(msgWithContextId(format), args); + } + + @Override + public void warn(String s, Object o, Object o1) { + this.logger.warn(msgWithContextId(s), o, o1); + } + + public void warn(String msg, Throwable t) { + this.logger.warn(msgWithContextId(msg), t); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return this.logger.isWarnEnabled(marker); + } + + @Override + public void warn(Marker marker, String s) { + this.logger.warn(marker, msgWithContextId(s)); + } + + @Override + public void warn(Marker marker, String s, Object o) { + this.logger.warn(marker, msgWithContextId(s), o); + } + + @Override + public void warn(Marker marker, String s, Object o, Object o1) { + this.logger.warn(marker, msgWithContextId(s), o, o1); + } + + @Override + public void warn(Marker marker, String s, Object... objects) { + this.logger.warn(marker, msgWithContextId(s), objects); + } + + @Override + public void warn(Marker marker, String s, Throwable throwable) { + this.logger.warn(marker, msgWithContextId(s), throwable); + } + + @Override + public void error(String msg) { + this.logger.error(msgWithContextId(msg)); + } + + @Override + public void error(String msg, Object arg) { + this.logger.error(msgWithContextId(msg), arg); + } + + @Override + public void error(String s, Object o, Object o1) { + this.logger.error(msgWithContextId(s), o, o1); + } + + @Override + public void error(String format, Object... args) { + this.logger.error(msgWithContextId(format), args); + } + + @Override + public void error(String msg, Throwable t) { + this.logger.error(msgWithContextId(msg), t); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return this.logger.isErrorEnabled(marker); + } + + @Override + public void error(Marker marker, String s) { + this.logger.error(marker, msgWithContextId(s)); + } + + @Override + public void error(Marker marker, String s, Object o) { + this.logger.error(marker, msgWithContextId(s), o); + } + + @Override + public void error(Marker marker, String msg, Object arg1, Object arg2) { + this.logger.error(marker, msgWithContextId(msg), arg1, arg2); + } + + @Override + public void error(Marker marker, String s, Object... objects) { + this.logger.error(marker, msgWithContextId(s), objects); + } + + @Override + public void error(Marker marker, String s, Throwable throwable) { + this.logger.error(marker, msgWithContextId(s), throwable); + } + + @Override + public void trace(String msg) { + this.logger.trace(msgWithContextId(msg)); + } + + @Override + public void trace(String msg, Object arg) { + this.logger.trace(msgWithContextId(msg), arg); + } + + @Override + public void trace(String s, Object o, Object o1) { + this.logger.trace(msgWithContextId(s), o, o1); + } + + @Override + public void trace(String format, Object... args) { + this.logger.trace(msgWithContextId(format), args); + } + + @Override + public void trace(String msg, Throwable t) { + this.logger.trace(msgWithContextId(msg), t); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return this.logger.isTraceEnabled(marker); + } + + @Override + public void trace(Marker marker, String s) { + this.logger.trace(marker, msgWithContextId(s)); + } + + @Override + public void trace(Marker marker, String s, Object o) { + this.logger.trace(marker, msgWithContextId(s), o); + } + + @Override + public void trace(Marker marker, String s, Object o, Object o1) { + this.logger.trace(marker, msgWithContextId(s), o, o1); + } + + @Override + public void trace(Marker marker, String s, Object... objects) { + this.logger.trace(marker, msgWithContextId(s), objects); + } + + @Override + public void trace(Marker marker, String s, Throwable throwable) { + this.logger.trace(marker, msgWithContextId(s), throwable); + } + + @Override + public boolean isInfoEnabled() { + return this.logger.isInfoEnabled(); + } + + @Override + public boolean isDebugEnabled() { + return this.logger.isDebugEnabled(); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return this.logger.isDebugEnabled(marker); + } + + @Override + public void debug(Marker marker, String s) { + this.logger.debug(marker, msgWithContextId(s)); + } + + @Override + public void debug(Marker marker, String s, Object o) { + this.logger.debug(marker, msgWithContextId(s), o); + } + + @Override + public void debug(Marker marker, String s, Object o, Object o1) { + this.logger.debug(marker, msgWithContextId(s), o, o1); + } + + @Override + public void debug(Marker marker, String s, Object... objects) { + this.logger.debug(marker, msgWithContextId(s), objects); + } + + @Override + public void debug(Marker marker, String s, Throwable throwable) { + this.logger.debug(marker, msgWithContextId(s), throwable); + } + + @Override + public boolean isWarnEnabled() { + return this.logger.isWarnEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return this.logger.isErrorEnabled(); + } + + @Override + public String getName() { + return this.logger.getName(); + } + + @Override + public boolean isTraceEnabled() { + return this.logger.isTraceEnabled(); + } + + public String getContextId() { + return this.contextId; + } + + private String msgWithContextId(String msg) { + if (this.contextId == null) { + return msg; + } else { + return CTX + this.contextId + CLOSING_BRACKET + "\t" + msg; + } + } +} diff --git a/src/main/java/org/whitesource/agent/utils/LoggerFactory.java b/src/main/java/org/whitesource/agent/utils/LoggerFactory.java new file mode 100644 index 0000000..db057f9 --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/LoggerFactory.java @@ -0,0 +1,17 @@ +package org.whitesource.agent.utils; + +/** + * @author raz.nitzan + */ +public class LoggerFactory { + + public static String contextId; + + public static LoggerFS getLogger(Class clazz) { + return new LoggerFS(clazz, contextId); + } + + public static LoggerFS getLogger(String name) { + return new LoggerFS(name, contextId); + } +} diff --git a/src/main/java/org/whitesource/agent/utils/MemoryUsageHelper.java b/src/main/java/org/whitesource/agent/utils/MemoryUsageHelper.java new file mode 100644 index 0000000..fec1ce5 --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/MemoryUsageHelper.java @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.agent.utils; + +/** + * @author eugen.horovitz + * Class helper for tracking memory usage + * + */ +public class MemoryUsageHelper { + + /** + * + * @return Gets memory usage + */ + public static SystemStats getMemoryUsage() { + + long mbRatio = 1024*1024; + Runtime runTime = Runtime.getRuntime(); + + return new SystemStats(runTime.availableProcessors(),runTime.freeMemory()/mbRatio, + runTime.maxMemory()/mbRatio,runTime.totalMemory()/mbRatio, + (runTime.totalMemory()-runTime.freeMemory())/mbRatio); + } + + public static class SystemStats { + int availableProcessors; + long freeMemory; + long maxMemory; + long totalMemory; + long usedMemory; + + private SystemStats(int availableProcessors, long freeMemory, long maxMemory, long totalMemory, long usedMemory) { + this.availableProcessors = availableProcessors; + this.freeMemory = freeMemory; + this.maxMemory = maxMemory; + this.totalMemory = totalMemory; + this.usedMemory = usedMemory; + } + + public int getAvailableProcessors() { + return availableProcessors; + } + + public long getFreeMemory() { + return freeMemory; + } + + public long getMaxMemory() { + return maxMemory; + } + + public long getTotalMemory() { + return totalMemory; + } + + public long getUsedMemory() { + return usedMemory; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + /* Total number of processors or cores available to the JVM */ + sb.append("Available processors (cores): \t" + getAvailableProcessors()); + sb.append(System.lineSeparator()); + + /* Total amount of free memory available to the JVM */ + sb.append("Free memory (Mb): \t" + + getFreeMemory()); + sb.append(System.lineSeparator()); + + /* This will return Long.MAX_VALUE if there is no preset limit */ + /* Maximum amount of memory the JVM will attempt to use */ + sb.append("Max memory (Mb): \t" + (getMaxMemory() == Long.MAX_VALUE / 1024 / 1024 ? "no limit" : getMaxMemory())); + sb.append(System.lineSeparator()); + + /* Total memory currently in use by the JVM */ + sb.append("Total memory (Mb): \t" + getTotalMemory()); + sb.append(System.lineSeparator()); + + /* Used memory currently in use by the JVM */ + sb.append("Used memory (Mb): \t" + getUsedMemory()); + sb.append(System.lineSeparator()); + + return sb.toString(); + } + } +} diff --git a/src/main/java/org/whitesource/agent/utils/Pair.java b/src/main/java/org/whitesource/agent/utils/Pair.java new file mode 100644 index 0000000..fffe6b0 --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/Pair.java @@ -0,0 +1,22 @@ +package org.whitesource.agent.utils; + +public class Pair { + + /* --- Private members --- */ + private final X key; + private final Y value; + + /* --- Constructor --- */ + public Pair(X key, Y value) { + this.key = key; + this.value = value; + } + + /* --- Getters --- */ + public X getKey() { + return key; + } + public Y getValue() { + return value; + } +} diff --git a/src/main/java/org/whitesource/agent/utils/UniqueNamesGenerator.java b/src/main/java/org/whitesource/agent/utils/UniqueNamesGenerator.java new file mode 100644 index 0000000..703403f --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/UniqueNamesGenerator.java @@ -0,0 +1,27 @@ +package org.whitesource.agent.utils; + +import org.whitesource.agent.Constants; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +/** + * @author raz.nitzan + */ +public class UniqueNamesGenerator { + + /* --- public static methods --- */ + + public static String createUniqueName(String name, String extension) { + if (name == null) { + name = Constants.EMPTY_STRING; + } + if (extension == null) { + extension = Constants.EMPTY_STRING; + } + String creationDate = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + String uuid = UUID.randomUUID().toString(); + return name + Constants.UNDERSCORE + creationDate + Constants.UNDERSCORE + uuid + extension; + } +} diff --git a/src/main/java/org/whitesource/agent/utils/WsStringUtils.java b/src/main/java/org/whitesource/agent/utils/WsStringUtils.java new file mode 100644 index 0000000..fc5fab4 --- /dev/null +++ b/src/main/java/org/whitesource/agent/utils/WsStringUtils.java @@ -0,0 +1,50 @@ +package org.whitesource.agent.utils; + +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.fs.FSAConfigProperty; +import org.whitesource.fs.WsSecret; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; + +public class WsStringUtils { + + private static final Logger logger = LoggerFactory.getLogger(WsStringUtils.class); + + public static String toString(Object obj) { + StringBuilder result = new StringBuilder(); + + Field[] fields = obj.getClass().getDeclaredFields(); + + for (Field field : fields) { + if (field.isAnnotationPresent(FSAConfigProperty.class)) { + field.setAccessible(true); + try { + Object value = field.get(obj); + + Class fieldType = field.getType(); + + if (value == null) { + result.append(field.getName() + Constants.EQUALS + Constants.EMPTY_STRING + Constants.NEW_LINE); + } else { + if(field.isAnnotationPresent(WsSecret.class)){ + result.append(field.getName() + Constants.EQUALS + field.getAnnotation(WsSecret.class).value() + Constants.NEW_LINE); + } else if (fieldType.isArray()){ + result.append(field.getName() + Constants.EQUALS + Arrays.toString((Object[])value) + Constants.NEW_LINE); + } else if (fieldType.isPrimitive() || fieldType.isAssignableFrom(String.class) + || fieldType.isAssignableFrom(Boolean.class) || Collection.class.isAssignableFrom(fieldType)){ + result.append(field.getName() + Constants.EQUALS + value + Constants.NEW_LINE); + } else { + result.append(value.toString()); + } + } + } catch (IllegalAccessException e) { + logger.debug("Failed in WsStringUtils toString - {}. Exception: {}", e.getMessage(), e.getStackTrace()); + } + } + } + return result.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/contracts/PluginInfo.java b/src/main/java/org/whitesource/contracts/PluginInfo.java new file mode 100644 index 0000000..e09791a --- /dev/null +++ b/src/main/java/org/whitesource/contracts/PluginInfo.java @@ -0,0 +1,7 @@ +package org.whitesource.contracts; + +public interface PluginInfo { + String getAgentType(); + String getAgentVersion(); + String getPluginVersion(); +} diff --git a/src/main/java/org/whitesource/fs/CommandLineArgs.java b/src/main/java/org/whitesource/fs/CommandLineArgs.java new file mode 100644 index 0000000..cf3703f --- /dev/null +++ b/src/main/java/org/whitesource/fs/CommandLineArgs.java @@ -0,0 +1,180 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.converters.CommaParameterSplitter; + import org.whitesource.agent.Constants; + +import java.util.LinkedList; +import java.util.List; + +/** + * Author: Itai Marko + */ +public class CommandLineArgs { + + /* --- Static members --- */ + + public static final String CONFIG_FILE_NAME = "whitesource-fs-agent.config"; + + /* --- Parameters --- */ + + @Parameter(names = "-c", description = "Config file path") + String configFilePath = CONFIG_FILE_NAME; + + //TODO use a File converter for dependencyDir and configFilePath + @Parameter(names = "-d", splitter = CommaParameterSplitter.class, description = "Comma separated list of directories and/or files to scan") + List dependencyDirs = new LinkedList<>(); // TODO this may be a bad default, consider printing usage instead + + @Parameter(names = "-f", description = "File list path") + String fileListPath = Constants.EMPTY_STRING; + + @Parameter(names = "-apiKey", description = "Organization api key") + String apiKey = null; + + @Parameter(names = "-product", description = "Product name or token") + String product = null; + + @Parameter(names = "-productVersion", description = "Product version") + String productVersion = null; + + @Parameter(names = "-project", description = "Project name or token") + String project = null; + + @Parameter(names = "-projectVersion", description = "Project version") + String projectVersion = null; + + @Parameter(names = "-proxy.host", description = "Proxy Host") + String proxyHost = null; + + @Parameter(names = "-proxy.port", description = "Proxy Port") + String proxyPort = null; + + @Parameter(names = "-proxy.user", description = "Proxy User") + String proxyUser = null; + + @Parameter(names = "-proxy.pass", description = "Proxy Password") + String proxyPass = null; + + @Parameter(names = "-proxy", description = "Proxy info in format: scheme://:@host:port/") + String proxy = null; + + @Parameter(names = "-archiveFastUnpack", description = "Fast unpack") + String archiveFastUnpack = "false"; + + @Parameter(names = "-requestFiles", description = "Comma separated list of paths to offline request files") + List requestFiles = new LinkedList<>(); + + @Parameter(names = "-projectPerFolder", description = "Creates a project for each subfolder, the subfolder's name is used as the project name") + String projectPerFolder = Constants.EMPTY_STRING; + + @Parameter(names = "-updateType", description = "Specify if the project dependencies should be removed before adding the new ones") + String updateType = Constants.EMPTY_STRING; + + @Parameter(names = "-scm.repositoriesFile", description = "Specify the csv file from which scm repositories should be loaded") + String repositoriesFile = null; + + @Parameter(names = "-offline", description = "Whether or not to create an offline update request instead of sending one to WhiteSource") + String offline = null; + + @Parameter(names = "-web", description = "Whether or not to create a web service on startup that receives requests from outside") + String web = "false"; + + @Parameter(names = "-whiteSourceFolderPath", description = "WhiteSource folder path for offlineRequest/checkPolicies") + String whiteSourceFolder = Constants.EMPTY_STRING; + + @Parameter(names = "-appPath", description = "Impact Analysis application path") + List appPath = new LinkedList<>(); + + @Parameter(names = "-xPaths", description = "Path to impact Analysis application paths and directories") + String xPaths = null; + + @Parameter(names = "-viaDebug", description = "Impact Analysis debug flag") + String viaDebug = null; + + @Parameter(names = "-viaLevel", description = "Impact Analysis level") + String viaLevel = "1"; + + @Parameter(names = "-enableImpactAnalysis", description = "Whether or not to enable impact analysis") + String enableImpactAnalysis = null; + + @Parameter(names = "-iaLanguage", description = "Impact analysis language") + String iaLanguage = null; + + @Parameter(names = "-userKey", description = "user key uniquely identifying the account at white source") + String userKey = null; + + @Parameter(names = "-sendLogsToWss", description = "whether to send logs to WhiteSource or not") + String sendLogsToWss = null; + + @Parameter(names = "-scanComment", description = "scan comment") + String scanComment = null; + + + @Parameter(names = "-projectToken", description = "API token to match an existing WhiteSource project") + String projectToken = null; + + @Parameter(names = "-productToken", description = "Unique identifier of the product to update") + String productToken = null; + + @Parameter(names = "-logLevel", description = "log level of the project") + String logLevel = null; + + @Parameter(names = "-requirementsFileIncludes", description = "List of dependency files split by comma") + List requirementsFileIncludes = new LinkedList<>(); + + @Parameter(names = "-logContext", description = "Context id for logger") + String logContext = null; + + @Parameter(names = "-requireKnownSha1", description = "User-entry of a flag that overrides default FSA process termination when sha1 is missing in case of via") + String requireKnownSha1 = null; + + @Parameter(names = "-analyzeMultiModule", description = "The parameter instructs the FSA to inspect the structure of a specified multi-module" + + " and save the project name for each sub-module in a setup file") + String analyzeMultiModule = null; + + @Parameter(names = "-xModulePath", description = "The parameter get setup file and read the appPaths and -d parameter for via") + String xModulePath = null; + + @Parameter(names = "-docker.scanImages", description = "Boolean Parameter, decides if to scan docker images if true or given folder if false") + String scanDockerImages = null; + + + @Parameter(names = "-addSha1", description = "for developement only; false by default") + String addSha1 = null; + + @Parameter(names = "-wss.url", description = "URL to send the request to") + String wssUrl = null; + + @Parameter(names = "-noConfig", description = "Run without a config file") + String noConfig = null; + + /* --- Public methods --- */ + + public String getConfigFilePath() { + return configFilePath; + } + + public void parseCommandLine(String[] args) { + JCommander jCommander = new JCommander(); + jCommander.setCaseSensitiveOptions(false); + jCommander.addObject(this); + jCommander.parse(args); + } + +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/ComponentScan.java b/src/main/java/org/whitesource/fs/ComponentScan.java new file mode 100644 index 0000000..01c6b24 --- /dev/null +++ b/src/main/java/org/whitesource/fs/ComponentScan.java @@ -0,0 +1,118 @@ +package org.whitesource.fs; + +import org.slf4j.Logger; +import org.whitesource.agent.ProjectConfiguration; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.ConfigPropertyKeys; +import org.whitesource.agent.Constants; +import org.whitesource.agent.FileSystemScanner; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.DependencyInfo; +import org.whitesource.agent.dependency.resolver.docker.DockerResolver; +import org.whitesource.agent.dependency.resolver.packageManger.PackageManagerExtractor; +import org.whitesource.fs.configuration.ConfigurationSerializer; +import org.whitesource.fs.configuration.ResolverConfiguration; + +import java.util.*; + + +/** + * Created by anna.rozin + */ +public class ComponentScan { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(ComponentScan.class); + + /* --- Members --- */ + + private FSAConfigProperties config; + + /* --- Constructors --- */ + + public ComponentScan(FSAConfigProperties config) { + this.config = config; + } + + /* --- Methods --- */ + + public String scan() { + logger.info("Starting analysis - component scan has started"); + String directory = config.getProperty(Constants.DIRECTORY); + String[] directories = directory.split(Constants.COMMA); + List scannerBaseDirs = new ArrayList<>(Arrays.asList(directories)); + if (!scannerBaseDirs.isEmpty()) { + logger.info("Getting properties"); + // configure properties + FSAConfiguration fsaConfiguration = new FSAConfiguration(config); + // set default values in case of missing parameters + ResolverConfiguration resolverConfiguration = fsaConfiguration.getResolver(); + String[] includes = config.getProperty(ConfigPropertyKeys.INCLUDES_PATTERN_PROPERTY_KEY) != null ? + config.getProperty(ConfigPropertyKeys.INCLUDES_PATTERN_PROPERTY_KEY).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX) : ExtensionUtils.INCLUDES; + String[] excludes = config.getProperty(ConfigPropertyKeys.EXCLUDES_PATTERN_PROPERTY_KEY) != null ? + config.getProperty(ConfigPropertyKeys.EXCLUDES_PATTERN_PROPERTY_KEY).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX) : ExtensionUtils.EXCLUDES; + String[] acceptExtensionsList = (String[]) config.get(ConfigPropertyKeys.ACCEPT_EXTENSIONS_LIST); + boolean globCaseSensitive = config.getProperty(ConfigPropertyKeys.CASE_SENSITIVE_GLOB_PROPERTY_KEY) != null ? + Boolean.valueOf(config.getProperty(ConfigPropertyKeys.CASE_SENSITIVE_GLOB_PROPERTY_KEY)) : false; + boolean followSymlinks = config.getProperty(ConfigPropertyKeys.CASE_SENSITIVE_GLOB_PROPERTY_KEY) != null ? + Boolean.valueOf(config.getProperty(ConfigPropertyKeys.CASE_SENSITIVE_GLOB_PROPERTY_KEY)) : false; + Collection excludedCopyrights = fsaConfiguration.getAgent().getExcludedCopyrights(); + excludedCopyrights.remove(Constants.EMPTY_STRING); + //todo hasScmConnectors[0] in future - no need for cx + // Resolving dependencies + logger.info("Resolving dependencies"); + // via should not run for componentScan + Set setDirs = new HashSet<>(); + setDirs.addAll(scannerBaseDirs); + Map> appPathsToDependencyDirs = new HashMap<>(); + appPathsToDependencyDirs.put(FSAConfiguration.DEFAULT_KEY, setDirs); + Collection projects; + // scan packageManager||Docker||Regular Scan + if (Boolean.valueOf(config.getProperty(ConfigPropertyKeys.SCAN_PACKAGE_MANAGER))) { + projects = new PackageManagerExtractor().createProjects(); + } else if (Boolean.valueOf(config.getProperty(ConfigPropertyKeys.SCAN_DOCKER_IMAGES))) { + projects = new DockerResolver(fsaConfiguration).resolveDockerImages(); + } else { + ProjectConfiguration projectConfiguration = new ProjectConfiguration(fsaConfiguration.getAgent(), scannerBaseDirs, appPathsToDependencyDirs, false); + projects = new FileSystemScanner(resolverConfiguration, fsaConfiguration.getAgent(), false).createProjects(projectConfiguration).keySet(); + } + + logger.info("Finished dependency resolution"); + for (AgentProjectInfo project : projects) { + project.setProjectToken(Constants.WHITESPACE); + if (acceptExtensionsList != null && acceptExtensionsList.length > 0) { + project.setDependencies(getDependenciesFromExtensionsListOnly(project.getDependencies(), acceptExtensionsList)); + } + } + // Return dependencies + String jsonString = new ConfigurationSerializer().getAsString(projects, true); + return jsonString; + } else { + return Constants.EMPTY_STRING;// new ConfigurationSerializer<>().getAsString(new Collection); + } + } + + private List getDependenciesFromExtensionsListOnly(Collection dependencies, String[] acceptExtensionsList) { + LinkedList filteredDependencies = new LinkedList<>(); + for (DependencyInfo dependency : dependencies) { + for (String extension : acceptExtensionsList) { + if (dependency.getDependencyType() != null || dependency.getArtifactId().endsWith(Constants.DOT + extension) || checkFileName(dependency, extension)) { + filteredDependencies.add(dependency); + dependency.setChildren(getDependenciesFromExtensionsListOnly(dependency.getChildren(), acceptExtensionsList)); + break; + } + } + } + return filteredDependencies; + } + + private boolean checkFileName(DependencyInfo dependency, String extension) { + boolean fileNameEndsWithExtension = false; + if (dependency.getFilename() != null) { + fileNameEndsWithExtension = dependency.getFilename().endsWith(Constants.DOT + extension); + } + return fileNameEndsWithExtension; + } +} + diff --git a/src/main/java/org/whitesource/fs/ExtensionUtils.java b/src/main/java/org/whitesource/fs/ExtensionUtils.java new file mode 100644 index 0000000..87da234 --- /dev/null +++ b/src/main/java/org/whitesource/fs/ExtensionUtils.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2016 WhiteSource Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + + +import org.whitesource.agent.Constants; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This class contains all supported file extensions and creates regex and GLOB patterns for the directory scanner. + * + * @author tom.shapira + */ +public class ExtensionUtils { + + /* --- Static members --- */ + + public static final List SOURCE_EXTENSIONS = Arrays.asList( + "c", "cc", "cp", "cpp", "cxx", "c\\+\\+", "h", "hh", "hpp", "hxx", "h\\+\\+", "m", "mm", "pch", // C and C++ + "c#", "cs", "csharp", // C# + "go", "goc", // GO + "js", // JavaScript + "pl", "plx", "pm", "ph", "cgi", "fcgi", "pod", "psgi", "al", "perl", "t", // PERL + "pl6", "p6m", "p6l", "pm6", "nqp", "6pl", "6pm", "p6", // PERL6 + "php", // PHP + "py", // Python + "rb", // Ruby + "swift", // Swift + "java", // Java + "clj", "cljx", "cljs", "cljc"); // Clojure + public static final List BINARY_EXTENSIONS = Arrays.asList("jar", "egg", "tar.gz", "tgz", "zip", "whl", "gem", + "apk", "air", "dmg", "exe", "gem", "gzip", "msi", "nupkg", "swc", "swf", "tar.bz2", "pkg.tar.xz", "(u)?deb", "(a)?rpm"); + public static final List ARCHIVE_EXTENSIONS = Arrays.asList("war", "ear", "zip", "whl", "tar.gz", "tgz", "tar"); + + public static final String SOURCE_FILE_PATTERN; + public static final String BINARY_FILE_PATTERN; + public static final String ARCHIVE_FILE_PATTERN; + public static final String[] INCLUDES; + public static final String[] EXCLUDES = new String[] { "**/*sources.jar", "**/*javadoc.jar", "**/tests/**" }; + public static final String[] ARCHIVE_INCLUDES; + public static final String[] ARCHIVE_EXCLUDES = new String[] { "**/*sources.jar", "**/*javadoc.jar", "**/tests/**" }; + + static { + SOURCE_FILE_PATTERN = initializeRegexPattern(SOURCE_EXTENSIONS); + BINARY_FILE_PATTERN = initializeRegexPattern(BINARY_EXTENSIONS); + ARCHIVE_FILE_PATTERN = initializeRegexPattern(ARCHIVE_EXTENSIONS); + List allExtensions = new ArrayList<>(); + allExtensions.addAll(SOURCE_EXTENSIONS); + allExtensions.addAll(BINARY_EXTENSIONS); + INCLUDES = initializeGlobPattern(allExtensions); + ARCHIVE_INCLUDES = initializeGlobPattern(ARCHIVE_EXTENSIONS); + } + + private static String initializeRegexPattern(List extensions) { + StringBuilder sb = new StringBuilder(); + for (String extension : extensions) { + sb.append(Constants.REGEX_PATTERN_PREFIX); + sb.append(extension); + sb.append(Constants.PIPE); + } + return sb.toString().substring(0, sb.toString().lastIndexOf(Constants.PIPE)); + } + + private static String[] initializeGlobPattern(List extensions) { + String[] globPatterns = new String[extensions.size()]; + for (int i = 0; i < extensions.size(); i++) { + globPatterns[i] = Constants.GLOB_PATTERN_PREFIX + Constants.DOT + extensions.get(i); + } + return globPatterns; + } +} diff --git a/src/main/java/org/whitesource/fs/FSAConfigProperties.java b/src/main/java/org/whitesource/fs/FSAConfigProperties.java new file mode 100644 index 0000000..5b6a066 --- /dev/null +++ b/src/main/java/org/whitesource/fs/FSAConfigProperties.java @@ -0,0 +1,153 @@ +package org.whitesource.fs; + +import org.apache.commons.lang.StringUtils; +import org.whitesource.agent.ConfigPropertyKeys; +import org.whitesource.agent.Constants; + +import java.util.Properties; + +/* + * FSA Configuration File Properties Helper. + */ +public class FSAConfigProperties extends Properties { + + @Override + public synchronized Object setProperty(String key, String value) { + return super.setProperty(key.toLowerCase(), value); + } + + @Override + public String getProperty(String key) { + + return super.getProperty(key.toLowerCase()); + } + + @Override + public synchronized Object put(Object key, Object value) { + if (key instanceof String) { + return super.put(((String) key).toLowerCase(), value); + } else { + return super.put(key, value); + } + } + + @Override + public synchronized Object get(Object key) { + if (key instanceof String) { + return super.get(((String) key).toLowerCase()); + } else { + return super.get(key); + } + } + + + public int getIntProperty(String propertyKey, int defaultValue) { + int value = defaultValue; + String propertyValue = getProperty(propertyKey); + if (StringUtils.isNotBlank(propertyValue)) { + try { + value = Integer.valueOf(propertyValue); + } catch (NumberFormatException e) { + // do nothing + } + } + return value; + } + + public boolean getBooleanProperty(String propertyKey, boolean defaultValue) { + boolean property = defaultValue; + String propertyValue = getProperty(propertyKey); + if (StringUtils.isNotBlank(propertyValue)) { + property = Boolean.valueOf(propertyValue); + } + return property; + } + + public boolean getBooleanProperty(String propertyKey, String dominantPropertyKey, boolean defaultValue) { + boolean property = defaultValue; + String propertyValue = getProperty(propertyKey); + String dominantPropertyValue = getProperty(dominantPropertyKey); + if (StringUtils.isNotBlank(dominantPropertyValue)) { + property = Boolean.valueOf(dominantPropertyValue); + } else if (StringUtils.isNotBlank(propertyValue)) { + property = Boolean.valueOf(propertyValue); + } + return property; + } + + public long getLongProperty(String propertyKey, long defaultValue) { + long property = defaultValue; + String propertyValue = getProperty(propertyKey); + if (StringUtils.isNotBlank(propertyValue)) { + property = Long.parseLong(propertyValue); + } + return property; + } + + public String[] getListProperty(String propertyName, String[] defaultValue) { + String property = getProperty(propertyName); + if (property == null) { + return defaultValue; + } + return property.split(Constants.WHITESPACE); + } + + public String[] getPythonIncludesWithPipfile(String propertyName, String[] defaultValue) { + String property = getProperty(propertyName); + if (property == null) { + return defaultValue; + } + property = property + Constants.WHITESPACE + Constants.PIPFILE; + return property.split(Constants.WHITESPACE); + } + + + public int getArchiveDepth() { + return getIntProperty(ConfigPropertyKeys.ARCHIVE_EXTRACTION_DEPTH_KEY, FSAConfiguration.DEFAULT_ARCHIVE_DEPTH); + } + + public String[] getIncludes() { + String includesString = getProperty(ConfigPropertyKeys.INCLUDES_PATTERN_PROPERTY_KEY, Constants.EMPTY_STRING); + if (StringUtils.isNotBlank(includesString)) { + return getProperty(ConfigPropertyKeys.INCLUDES_PATTERN_PROPERTY_KEY, Constants.EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } + return new String[0]; + } + + public String[] getPythonIncludes() { + String includesString = getProperty(ConfigPropertyKeys.PYTHON_REQUIREMENTS_FILE_INCLUDES, Constants.PYTHON_REQUIREMENTS); + if (StringUtils.isNotBlank(includesString)) { + return getProperty(ConfigPropertyKeys.PYTHON_REQUIREMENTS_FILE_INCLUDES, Constants.PYTHON_REQUIREMENTS).split(Constants.WHITESPACE); + } + return new String[0]; + } + + public String[] getProjectPerFolderIncludes() { + String projectPerFolderIncludesString = getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_INCLUDES, null); + if (StringUtils.isNotBlank(projectPerFolderIncludesString)) { + return getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_INCLUDES, Constants.EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } + if (Constants.EMPTY_STRING.equals(projectPerFolderIncludesString)) { + return null; + } + String[] result = new String[1]; + result[0] = "*"; + return result; + } + + public String[] getProjectPerFolderExcludes() { + String projectPerFolderExcludesString = getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_EXCLUDES, Constants.EMPTY_STRING); + if (StringUtils.isNotBlank(projectPerFolderExcludesString)) { + return getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_EXCLUDES, Constants.EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } + return new String[0]; + } + + public String[] getDockerIncludes() { + String includesString = getProperty(ConfigPropertyKeys.DOCKER_INCLUDES_PATTERN_PROPERTY_KEY, Constants.GLOB_PATTERN); + if (StringUtils.isEmpty(includesString)) { + setProperty(ConfigPropertyKeys.DOCKER_INCLUDES_PATTERN_PROPERTY_KEY, Constants.GLOB_PATTERN); + } + return getProperty(ConfigPropertyKeys.DOCKER_INCLUDES_PATTERN_PROPERTY_KEY, Constants.GLOB_PATTERN).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } +} diff --git a/src/main/java/org/whitesource/fs/FSAConfigProperty.java b/src/main/java/org/whitesource/fs/FSAConfigProperty.java new file mode 100644 index 0000000..af1d1cf --- /dev/null +++ b/src/main/java/org/whitesource/fs/FSAConfigProperty.java @@ -0,0 +1,11 @@ +package org.whitesource.fs; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface FSAConfigProperty { +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/FSAConfiguration.java b/src/main/java/org/whitesource/fs/FSAConfiguration.java new file mode 100644 index 0000000..082f2c1 --- /dev/null +++ b/src/main/java/org/whitesource/fs/FSAConfiguration.java @@ -0,0 +1,1291 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.whitesource.agent.ConfigPropertyKeys; +import org.whitesource.agent.Constants; +import org.whitesource.agent.ViaLanguage; +import org.whitesource.agent.api.dispatch.UpdateType; +import org.whitesource.agent.client.ClientConstants; +import org.whitesource.agent.dependency.resolver.maven.MavenTreeDependencyCollector; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.Pair; +import org.whitesource.agent.utils.WsStringUtils; +import org.whitesource.fs.configuration.*; + +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.whitesource.agent.Constants.COLON; +import static org.whitesource.agent.Constants.EMPTY_STRING; +import static org.whitesource.agent.client.ClientConstants.SERVICE_URL_KEYWORD; + +/** + * Author: eugen.horovitz + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class FSAConfiguration { + + /* --- Static members --- */ + + public static Collection ignoredWebProperties = Arrays.asList( + ConfigPropertyKeys.SCM_REPOSITORIES_FILE, ConfigPropertyKeys.LOG_LEVEL_KEY, ConfigPropertyKeys.FOLLOW_SYMBOLIC_LINKS, ConfigPropertyKeys.SHOW_PROGRESS_BAR, ConfigPropertyKeys.PROJECT_CONFIGURATION_PATH, ConfigPropertyKeys.SCAN_PACKAGE_MANAGER, ConfigPropertyKeys.WHITESOURCE_FOLDER_PATH, + ConfigPropertyKeys.ENDPOINT_ENABLED, ConfigPropertyKeys.ENDPOINT_PORT, ConfigPropertyKeys.ENDPOINT_CERTIFICATE, ConfigPropertyKeys.ENDPOINT_PASS, ConfigPropertyKeys.ENDPOINT_SSL_ENABLED, ConfigPropertyKeys.OFFLINE_PROPERTY_KEY, ConfigPropertyKeys.OFFLINE_ZIP_PROPERTY_KEY, + ConfigPropertyKeys.OFFLINE_PRETTY_JSON_KEY, ConfigPropertyKeys.WHITESOURCE_CONFIGURATION, ConfigPropertyKeys.SCANNED_FOLDERS); + + public static final int VIA_DEFAULT_ANALYSIS_LEVEL = 1; + public static final String DEFAULT_KEY = "defaultKey"; + public static final String APP_PATH = "-appPath"; + private static final String FALSE = "false"; + private static final String INFO = "info"; + public static final String INCLUDES_EXCLUDES_SEPARATOR_REGEX = "[,;\\s]+"; + public static final int DEFAULT_ARCHIVE_DEPTH = 0; + private static final String NONE = "(none)"; + public static final String WHITE_SOURCE_DEFAULT_FOLDER_PATH = "."; + public static final String PIP = "pip"; + public static final String PYTHON = "python"; + public static final String SERVICE_URL_REGEX = "http[s]?:\\/\\/[-\\w\\/=:.]*\\/agent"; + + public static final int DEFAULT_PORT = 443; + public static final boolean DEFAULT_SSL = true; + private static final boolean DEFAULT_ENABLED = false; + + @FSAConfigProperty + private boolean projectPerFolder; + @FSAConfigProperty + private int connectionTimeOut; + + + /* --- Private fields --- */ + + private final ScmConfiguration scm; + @FSAConfigProperty + private SenderConfiguration sender; + @FSAConfigProperty + private final OfflineConfiguration offline; + @FSAConfigProperty + private final ResolverConfiguration resolver; + private final ConfigurationValidation configurationValidation; + private final EndPointConfiguration endpoint; + private final RemoteDockerConfiguration remoteDockerConfiguration; + + private final List errors; + + /* --- Private final fields --- */ + + private final List offlineRequestFiles; + @FSAConfigProperty + private final String fileListPath; + @FSAConfigProperty + private List dependencyDirs; + @FSAConfigProperty + private final String configFilePath; + @FSAConfigProperty + private final AgentConfiguration agent; + @FSAConfigProperty + private final RequestConfiguration request; + private final List requirementsFileIncludes; + @FSAConfigProperty + private final boolean scanPackageManager; + @FSAConfigProperty + private final boolean scanDockerImages; + private final boolean scanTarImages; + private final boolean deleteTarImages; + + private final String scannedFolders; + + @FSAConfigProperty + private String logLevel; + private String logContext; + private boolean useCommandLineProductName; + private boolean useCommandLineProjectName; + private List appPaths; + private Map> appPathsToDependencyDirs; + private String analyzeMultiModule; + private String xModulePath; + private boolean setUpMuiltiModuleFile; + + /* --- Constructors --- */ + + public FSAConfiguration(FSAConfigProperties config) { + this(config, null); + } + + public FSAConfiguration() { + this(new FSAConfigProperties(), null); + } + + public FSAConfiguration(String[] args) { + this(null, args); + } + + public FSAConfiguration(FSAConfigProperties config, String[] args) { + configurationValidation = new ConfigurationValidation(); + String projectName; + errors = new ArrayList<>(); + appPathsToDependencyDirs = new HashMap<>(); + requirementsFileIncludes = new LinkedList<>(); + appPaths = null; + String apiToken = null; + String userKey = null; + String serviceUrl = null; + CommandLineArgs commandLineArgs = new CommandLineArgs(); + if ((args != null)) { + // read command line args + // validate args // TODO use jCommander validators + // TODO add usage command + commandLineArgs.parseCommandLine(args); + + if (config == null) { + analyzeMultiModule = commandLineArgs.analyzeMultiModule; + + // The config file is not necessary if there is the analyzeMultiModule parameter + if (Boolean.valueOf(commandLineArgs.noConfig)) { + config = new FSAConfigProperties(); + checkCmdArgsWithoutConfig(commandLineArgs); + } else if (StringUtils.isEmpty(analyzeMultiModule)) { + Pair> propertiesWithErrors = readWithError(commandLineArgs.configFilePath, commandLineArgs); + errors.addAll(propertiesWithErrors.getValue()); + config = propertiesWithErrors.getKey(); + } else { + config = new FSAConfigProperties(); + } + + if (StringUtils.isNotEmpty(commandLineArgs.project)) { + config.setProperty(ConfigPropertyKeys.PROJECT_NAME_PROPERTY_KEY, commandLineArgs.project); + } + } + + scannedFolders = config.getProperty(ConfigPropertyKeys.SCANNED_FOLDERS); + if (scannedFolders != null) { + String[] libsList = scannedFolders.split(Constants.COMMA); + // Trim all elements in libsList + Arrays.stream(libsList).map(String::trim).toArray(unused -> libsList); + dependencyDirs = Arrays.asList(libsList); + } + + configFilePath = commandLineArgs.configFilePath; + config.setProperty(ConfigPropertyKeys.PROJECT_CONFIGURATION_PATH, commandLineArgs.configFilePath); + + //override + offlineRequestFiles = updateProperties(config, commandLineArgs); + if (StringUtils.isNotEmpty(commandLineArgs.apiKey)) { + apiToken = commandLineArgs.apiKey; + } + if (StringUtils.isNotEmpty(commandLineArgs.userKey)) { + userKey = commandLineArgs.userKey; + } + if (StringUtils.isNotEmpty(commandLineArgs.wssUrl)) { + serviceUrl = commandLineArgs.wssUrl; + } + projectName = config.getProperty(ConfigPropertyKeys.PROJECT_NAME_PROPERTY_KEY); + fileListPath = commandLineArgs.fileListPath; + if (commandLineArgs.dependencyDirs != null && !commandLineArgs.dependencyDirs.isEmpty()) { + dependencyDirs = commandLineArgs.dependencyDirs; + } + appPaths = commandLineArgs.appPath; + if (StringUtils.isNotBlank(commandLineArgs.whiteSourceFolder)) { + config.setProperty(ConfigPropertyKeys.WHITESOURCE_FOLDER_PATH, commandLineArgs.whiteSourceFolder); + } + + // requirements file includes + requirementsFileIncludes.addAll(commandLineArgs.requirementsFileIncludes); + if (!requirementsFileIncludes.isEmpty()) { + String requirements = null; + for (String requirementFileIncludes : requirementsFileIncludes) { + if (requirements == null) { + requirements = requirementFileIncludes + Constants.WHITESPACE; + } else { + requirements += requirementFileIncludes + Constants.WHITESPACE; + } + } + requirements = requirements.substring(0, requirements.length() - 1); + config.setProperty(ConfigPropertyKeys.PYTHON_REQUIREMENTS_FILE_INCLUDES, requirements); + } + commandLineArgsOverride(commandLineArgs); + } else { + projectName = config.getProperty(ConfigPropertyKeys.PROJECT_NAME_PROPERTY_KEY); + configFilePath = NONE; + offlineRequestFiles = new ArrayList<>(); + fileListPath = null; + scannedFolders = null; + dependencyDirs = new ArrayList<>(); + commandLineArgsOverride(null); + } + + scanPackageManager = config.getBooleanProperty(ConfigPropertyKeys.SCAN_PACKAGE_MANAGER, false); + scanDockerImages = config.getBooleanProperty(ConfigPropertyKeys.SCAN_DOCKER_IMAGES, false); + scanTarImages = config.getBooleanProperty(ConfigPropertyKeys.SCAN_TAR_IMAGES, false); + deleteTarImages = config.getBooleanProperty(ConfigPropertyKeys.DELETE_TAR_FILES, true); + + if (dependencyDirs == null) + dependencyDirs = new ArrayList<>(); + + // validate scanned folder + if (dependencyDirs.isEmpty() && StringUtils.isEmpty(fileListPath) && (offlineRequestFiles == null || offlineRequestFiles.isEmpty())) { + dependencyDirs.add(Constants.DOT); + } + + // validate config + String projectToken = config.getProperty(ConfigPropertyKeys.PROJECT_TOKEN_PROPERTY_KEY); + String projectNameFinal = !StringUtils.isBlank(projectName) ? projectName : config.getProperty(ConfigPropertyKeys.PROJECT_NAME_PROPERTY_KEY); + projectPerFolder = config.getBooleanProperty(ConfigPropertyKeys.PROJECT_PER_SUBFOLDER, false); + if (StringUtils.isEmpty(apiToken)) { + apiToken = getToken(config, ConfigPropertyKeys.ORG_TOKEN_FILE, ConfigPropertyKeys.ORG_TOKEN_PROPERTY_KEY); + } + if (StringUtils.isEmpty(userKey)) { + userKey = getToken(config, ConfigPropertyKeys.USER_KEY_FILE, ConfigPropertyKeys.USER_KEY_PROPERTY_KEY); + } + int archiveExtractionDepth = config.getArchiveDepth(); + String[] includes = Constants.TRUE.equals(commandLineArgs.noConfig) ? ExtensionUtils.INCLUDES : config.getIncludes(); + String[] projectPerFolderIncludes = config.getProjectPerFolderIncludes(); + String[] pythonRequirementsFileIncludes = config.getPythonIncludes(); + String[] argsForAppPathAndDirs = args; + if (argsForAppPathAndDirs != null && argsForAppPathAndDirs.length == 0 && !dependencyDirs.isEmpty()) { + argsForAppPathAndDirs = dependencyDirs.toArray(new String[0]); + } + initializeDependencyDirs(argsForAppPathAndDirs, config); + String scanComment = config.getProperty(ConfigPropertyKeys.SCAN_COMMENT); + + // validate iaLanguage + validateIaLanguage(config); + + // todo: check possibility to get the errors only in the end + if (!Constants.TRUE.equals(commandLineArgs.noConfig)) { + errors.addAll(configurationValidation.getConfigurationErrors(projectPerFolder, projectToken, projectNameFinal, + apiToken, configFilePath, archiveExtractionDepth, includes, projectPerFolderIncludes, pythonRequirementsFileIncludes, scanComment)); + } + + logLevel = config.getProperty(ConfigPropertyKeys.LOG_LEVEL_KEY, INFO); + logContext = config.getProperty(ConfigPropertyKeys.LOG_CONTEXT); + // DO NOT CHANGE THE POSITION OF THE THREE LINES BELOW + if (StringUtils.isNotEmpty(logContext)) { + LoggerFactory.contextId = logContext; + } + + request = getRequest(config, apiToken, userKey, projectName, projectToken, scanComment); + scm = getScm(config); + agent = getAgent(config, commandLineArgs.noConfig); + offline = getOffline(config); + sender = null; + try { + sender = getSender(config, serviceUrl); + } catch (Exception e){ + errors.add(e.getMessage()); + } + resolver = getResolver(config); + endpoint = getEndpoint(config); + remoteDockerConfiguration = getRemoteDockerConfiguration(config); + + // The config file is not necessary if there is the analyzeMultiModule parameter + if (StringUtils.isEmpty(analyzeMultiModule)) { + // check properties to ensure via is ready to run + checkPropertiesForVia(sender, resolver, appPathsToDependencyDirs, errors); + } else { + errors.clear(); + if (args.length == 4 && dependencyDirs.size() == 1) { + Path path = Paths.get(analyzeMultiModule); + try { + if (!Files.exists(path)) { + File setUpFile = new File(analyzeMultiModule); + boolean fileCreated = setUpFile.createNewFile(); + if (fileCreated) { + setUpMuiltiModuleFile = true; + } else { + errors.add("The system could not create the multi-project setup file. Please contact support."); + } + } else { + errors.add("The file specified for storing multi-module analysis results already exists. Please specify a new file name."); + } + } catch (IOException e) { + errors.add("The system could not create the multi-project setup file : " + path + " " + "Please contact support."); + } + } else { + errors.add("Multi-module analysis could not run due to specified invalid parameters."); + } + } + } + + private void checkCmdArgsWithoutConfig(CommandLineArgs commandLineArgs) { + /* check if the minimum required settings for running without config file exist + apiKey & projectName/projectToken & productName/productToken & scannedDirectory + */ + if (commandLineArgs.apiKey == null || (commandLineArgs.projectToken == null && commandLineArgs.project == null) || commandLineArgs.dependencyDirs == null) { + errors.add("The apiKey and project/projectToken parameters are required to perform a scan without a configuration file"); + } + } + + private void validateIaLanguage(FSAConfigProperties config) { + String iaLanguage = config.getProperty(ConfigPropertyKeys.IA_LANGUAGE); + boolean iaLanguageValid = false; + if (iaLanguage != null) { + for (ViaLanguage viaLanguage : ViaLanguage.values()) { + if (iaLanguage.toLowerCase().equals(viaLanguage.toString().toLowerCase())) { + iaLanguageValid = true; + break; + } + } + if (!iaLanguageValid) { + errors.add("Error: VIA setting are not applicable parameters are not valid. exiting... "); + } + if (iaLanguageValid && !config.getBooleanProperty(ConfigPropertyKeys.ENABLE_IMPACT_ANALYSIS, false)) { + errors.add("Error: VIA setting are not applicable parameters are not valid. exiting... "); + } + } + } + + private String getToken(FSAConfigProperties config, String propertyKeyFile, String propertyKeyToken) { + String token = null; + String tokenFile = config.getProperty(propertyKeyFile); + if (StringUtils.isNotEmpty(tokenFile)) { + Pair> inputStreamAndErrors = getInputStreamFromFile(tokenFile, new CommandLineArgs()); + if (inputStreamAndErrors.getValue().isEmpty()) { + try { + token = new BufferedReader(new InputStreamReader(inputStreamAndErrors.getKey())).readLine(); + } catch (IOException e) { + errors.add("Error occurred when reading from " + tokenFile + e.getMessage()); + } + } else { + errors.addAll(inputStreamAndErrors.getValue()); + } + } else { + token = config.getProperty(propertyKeyToken); + } + return token; + } + + public void checkPropertiesForVia(SenderConfiguration sender, ResolverConfiguration resolver, Map> appPathsToDependencyDirs, List errors) { + Set viaAppPaths = appPathsToDependencyDirs.keySet(); + if (sender != null && sender.isEnableImpactAnalysis()) { + // the default appPath size is one (defaultKey = -d property), this one check if the user set more then one appPath + if (viaAppPaths.size() == 1) { + errors.add("Effective Usage Analysis will not run if the command line parameter 'appPath' is not specified"); + } else { + boolean validViaAppPath = checkAppPathsForVia(viaAppPaths, errors); + if (validViaAppPath) { + if (resolver.getMavenIgnoredScopes() == null || (!Arrays.asList(resolver.getMavenIgnoredScopes()).contains(MavenTreeDependencyCollector.ALL) + && !Arrays.asList(resolver.getMavenIgnoredScopes()).contains(MavenTreeDependencyCollector.NONE)) + || !resolver.isMavenAggregateModules() || !resolver.isGradleAggregateModules()) { + errors.add("Effective Usage Analysis will not run if the following configuration file settings are not made: maven.ignoredScopes=All, " + + "maven.aggregateModules=true, gradle.aggregateModules=true"); + } + } + } + } else { + if (viaAppPaths.size() > 1) { + errors.add("Effective Usage Analysis will not run if the configuration file parameter enableImpactAnalysis is not set to 'true'"); + } + } + } + + // check validation for appPath property, first check if the path is exist and then if this path is not a directory + private boolean checkAppPathsForVia(Set keySet, List errors) { + for (String key : keySet) { + if (!key.equals("defaultKey")) { + File file = new File(key); + if (!file.exists()) { + errors.add("Effective Usage Analysis will not run if the -appPath parameter references an invalid file path. Check that the -appPath parameter specifies a valid path"); + return false; + } else if (!file.isFile()) { + errors.add("Effective Usage Analysis will not run if the -appPath parameter references an invalid file path. Check that the -appPath parameter specifies a valid path"); + return false; + } else { + return true; + } + } + } + return false; + } + + private void initializeDependencyDirs(String[] argsForAppPathAndDirs, FSAConfigProperties config) { + if (StringUtils.isNotEmpty(config.getProperty(ConfigPropertyKeys.X_PATHS))) { + try { + String textFromFile = new String(Files.readAllBytes(Paths.get(config.getProperty(ConfigPropertyKeys.X_PATHS))), StandardCharsets.UTF_8); + textFromFile = textFromFile.replaceAll(Constants.COMMA + Constants.WHITESPACE, Constants.COMMA); + textFromFile = textFromFile.replaceAll(System.lineSeparator(), Constants.WHITESPACE); + argsForAppPathAndDirs = textFromFile.split(Constants.WHITESPACE); + if (argsForAppPathAndDirs != null && argsForAppPathAndDirs.length > 0) { + initializeDependencyDirsToAppPath(argsForAppPathAndDirs); + } + for (String appPath : this.appPathsToDependencyDirs.keySet()) { + for (String dir : this.appPathsToDependencyDirs.get(appPath)) { + this.dependencyDirs.add(dir); + } + } + } catch (IOException e) { + errors.add("Error: Could not read the xPaths file: " + config.getProperty(ConfigPropertyKeys.X_PATHS)); + } + } else { + if (argsForAppPathAndDirs != null && argsForAppPathAndDirs.length > 0) { + initializeDependencyDirsToAppPath(argsForAppPathAndDirs); + } + } + } + + private EndPointConfiguration getEndpoint(FSAConfigProperties config) { + return new EndPointConfiguration(config.getIntProperty(ConfigPropertyKeys.ENDPOINT_PORT, DEFAULT_PORT), + config.getProperty(ConfigPropertyKeys.ENDPOINT_CERTIFICATE), + config.getProperty(ConfigPropertyKeys.ENDPOINT_PASS), + config.getBooleanProperty(ConfigPropertyKeys.ENDPOINT_ENABLED, DEFAULT_ENABLED), + config.getBooleanProperty(ConfigPropertyKeys.ENDPOINT_SSL_ENABLED, DEFAULT_SSL)); + } + + private ResolverConfiguration getResolver(FSAConfigProperties config) { + + boolean resolveAllDependencies = config.getBooleanProperty(ConfigPropertyKeys.RESOLVE_ALL_DEPENDENCIES, true); + // todo split this in multiple configuration before release fsa as a service + boolean npmRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.NPM_RUN_PRE_STEP, false); + boolean npmIgnoreScripts = config.getBooleanProperty(ConfigPropertyKeys.NPM_IGNORE_SCRIPTS, false); + boolean npmResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.NPM_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean npmIncludeDevDependencies = config.getBooleanProperty(ConfigPropertyKeys.NPM_INCLUDE_DEV_DEPENDENCIES, false); + + long npmTimeoutDependenciesCollector = config.getLongProperty(ConfigPropertyKeys.NPM_TIMEOUT_DEPENDENCIES_COLLECTOR_SECONDS, 60); + boolean npmIgnoreNpmLsErrors = config.getBooleanProperty(ConfigPropertyKeys.NPM_IGNORE_NPM_LS_ERRORS, false); + String npmAccessToken = config.getProperty(ConfigPropertyKeys.NPM_ACCESS_TOKEN); + boolean npmYarnProject = config.getBooleanProperty(ConfigPropertyKeys.NPM_YARN_PROJECT, false); + + boolean bowerResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.BOWER_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean bowerRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.BOWER_RUN_PRE_STEP, false); + + boolean nugetResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.NUGET_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean nugetRestoreDependencies = config.getBooleanProperty(ConfigPropertyKeys.NUGET_RESTORE_DEPENDENCIES, false); + boolean nugetRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.NUGET_RUN_PRE_STEP, false); + boolean nugetResolvePakcagesConfigFiles = config.getBooleanProperty(ConfigPropertyKeys.NUGET_RESOLVE_PACKAGES_CONFIG_FILES, true); + boolean nugetResolveCsProjFiles = config.getBooleanProperty(ConfigPropertyKeys.NUGET_RESOLVE_CS_PROJ_FILES, true); + + boolean mavenResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.MAVEN_RESOLVE_DEPENDENCIES, resolveAllDependencies); + String[] mavenIgnoredScopes = config.getListProperty(ConfigPropertyKeys.MAVEN_IGNORED_SCOPES, null); + boolean mavenAggregateModules = config.getBooleanProperty(ConfigPropertyKeys.MAVEN_AGGREGATE_MODULES, false); + boolean mavenIgnoredPomModules = config.getBooleanProperty(ConfigPropertyKeys.MAVEN_IGNORE_POM_MODULES, true); + boolean mavenRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.MAVEN_RUN_PRE_STEP, false); + boolean mavenIgnoreDependencyTreeErrors = config.getBooleanProperty(ConfigPropertyKeys.MAVEN_IGNORE_DEPENDENCY_TREE_ERRORS, false); + String whiteSourceConfiguration = config.getProperty(ConfigPropertyKeys.PROJECT_CONFIGURATION_PATH); + + boolean pythonResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_RESOLVE_DEPENDENCIES, resolveAllDependencies); + String pipPath = config.getProperty(ConfigPropertyKeys.PYTHON_PIP_PATH, PIP); + String pythonPath = config.getProperty(ConfigPropertyKeys.PYTHON_PATH, PYTHON); + boolean pythonIsWssPluginInstalled = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_IS_WSS_PLUGIN_INSTALLED, false); + boolean pythonUninstallWssPluginInstalled = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_UNINSTALL_WSS_PLUGIN, false); + boolean pythonIgnorePipInstallErrors = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_IGNORE_PIP_INSTALL_ERRORS, false); + boolean pythonInstallVirtualenv = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_INSTALL_VIRTUALENV, false); + boolean pythonResolveHierarchyTree = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_RESOLVE_HIERARCHY_TREE, true); + boolean pythonResolveSetupPyFiles = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_RESOLVE_SETUP_PY_FILES, false); + String[] bomPatternForPython; + if (pythonResolveSetupPyFiles) { + bomPatternForPython = new String[]{Constants.PYTHON_REQUIREMENTS, Constants.SETUP_PY, Constants.PIPFILE}; + //bomPatternForPython = new String[]{Constants.PATTERN + Constants.PYTHON_REQUIREMENTS, Constants.PATTERN + Constants.SETUP_PY, Constants.PATTERN + Constants.PIPFILE}; + } else { + bomPatternForPython = new String[]{Constants.PYTHON_REQUIREMENTS, Constants.PIPFILE}; + //bomPatternForPython = new String[]{Constants.PATTERN + Constants.PYTHON_REQUIREMENTS, Constants.PATTERN + Constants.PIPFILE}; + } + + String[] pythonRequirementsFileIncludes = config.getPythonIncludesWithPipfile(ConfigPropertyKeys.PYTHON_REQUIREMENTS_FILE_INCLUDES, bomPatternForPython); + boolean pythonRunPipenvPreStep = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_RUN_PIPENV_PRE_STEP, false); + boolean pythonIgnorePipenvInstallErrors = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_IGNORE_PIPENV_INSTALL_ERRORS, false); + boolean pythonInstallDevDependencies = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_PIPENV_DEV_DEPENDENCIES, false); + + boolean gradleResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.GRADLE_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean gradleRunAssembleCommand = config.getBooleanProperty(ConfigPropertyKeys.GRADLE_RUN_ASSEMBLE_COMMAND, true); + boolean gradleAggregateModules = config.getBooleanProperty(ConfigPropertyKeys.GRADLE_AGGREGATE_MODULES, false); + boolean gradleRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.GRADLE_RUN_PRE_STEP, false); + String[] gradleIgnoredScopes = config.getListProperty(ConfigPropertyKeys.GRADLE_IGNORE_SCOPES, new String[0]); + String graldeLocalRepositoryPath = config.getProperty(ConfigPropertyKeys.GRADLE_LOCAL_REPOSITORY_PATH, EMPTY_STRING); + String gradlePreferredEnvironment = config.getProperty(ConfigPropertyKeys.GRADLE_PREFERRED_ENVIRONMENT, Constants.GRADLE); + if (gradlePreferredEnvironment.isEmpty()) { + gradlePreferredEnvironment = Constants.GRADLE; + } + + boolean paketResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.PAKET_RESOLVE_DEPENDENCIES, resolveAllDependencies); + String[] paketIgnoredScopes = config.getListProperty(ConfigPropertyKeys.PAKET_IGNORED_GROUPS, null); + boolean paketRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.PAKET_RUN_PRE_STEP, false); + String paketPath = config.getProperty(ConfigPropertyKeys.PAKET_EXE_PATH, null); + + boolean goResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.GO_RESOLVE_DEPENDENCIES, resolveAllDependencies); + String goDependencyManager = config.getProperty(ConfigPropertyKeys.GO_DEPENDENCY_MANAGER, EMPTY_STRING); + boolean goCollectDependenciesAtRuntime = config.getBooleanProperty(ConfigPropertyKeys.GO_COLLECT_DEPENDENCIES_AT_RUNTIME, false); + boolean goIgnoreTestPackages = config.getBooleanProperty(ConfigPropertyKeys.GO_GLIDE_IGNORE_TEST_PACKAGES, true); + boolean goGradleEnableTaskAlias = config.getBooleanProperty(ConfigPropertyKeys.GO_GRADLE_ENABLE_TASK_ALIAS, false); + boolean addSha1 = config.getBooleanProperty("addSha1", false); + + boolean rubyResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.RUBY_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean rubyRunBundleInstall = config.getBooleanProperty(ConfigPropertyKeys.RUBY_RUN_BUNDLE_INSTALL, false); + boolean rubyOverwriteGemFile = config.getBooleanProperty(ConfigPropertyKeys.RUBY_OVERWRITE_GEM_FILE, false); + boolean rubyInstallMissingGems = config.getBooleanProperty(ConfigPropertyKeys.RUBY_INSTALL_MISSING_GEMS, false); + + boolean phpResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.PHP_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean phpRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.PHP_RUN_PRE_STEP, false); + boolean phpIncludeDevDependencies = config.getBooleanProperty(ConfigPropertyKeys.PHP_INCLUDE_DEV_DEPENDENCIES, false); + + boolean sbtResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.SBT_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean sbtAggregateModules = config.getBooleanProperty(ConfigPropertyKeys.SBT_AGGREGATE_MODULES, false); + boolean sbtRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.SBT_RUN_PRE_STEP, false); + String sbtTargetFolder = config.getProperty(ConfigPropertyKeys.SBT_TARGET_FOLDER, EMPTY_STRING); + + boolean htmlResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.HTML_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean cocoapodsResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.COCOAPODS_RESOLVE_DEPENDENCIES, resolveAllDependencies); + boolean cocoapodsRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.COCOAPODS_RUN_PRE_STEP, false); + + // TODO - as long as there's no support for hex on the server side - the default value of hex.resolveDependencies is FALSE + boolean hexResolveDependencies = config.getBooleanProperty(ConfigPropertyKeys.HEX_RESOLVE_DEPENDENECIES, false); + boolean hexRunPreStep = config.getBooleanProperty(ConfigPropertyKeys.HEX_RUN_PRE_STEP, false); + boolean hexAggregateModules = config.getBooleanProperty(ConfigPropertyKeys.HEX_AGGREGATE_MODULES, false); + + boolean npmIgnoreSourceFiles; + boolean bowerIgnoreSourceFiles; + boolean nugetIgnoreSourceFiles; + boolean mavenIgnoreSourceFiles; + boolean pythonIgnoreSourceFiles; + boolean gradleIgnoreSourceFiles; + boolean paketIgnoreSourceFiles; + boolean sbtIgnoreSourceFiles; + boolean goIgnoreSourceFiles; + boolean rubyIgnoreSourceFiles; + boolean cocoapodsIgnoreSourceFiles; + boolean hexIgnoreSourceFiles; + boolean ignoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.IGNORE_SOURCE_FILES, ConfigPropertyKeys.DEPENDENCIES_ONLY, false); + + if (ignoreSourceFiles == true) { + npmIgnoreSourceFiles = true; + bowerIgnoreSourceFiles = true; + nugetIgnoreSourceFiles = true; + mavenIgnoreSourceFiles = true; + gradleIgnoreSourceFiles = true; + paketIgnoreSourceFiles = true; + sbtIgnoreSourceFiles = true; + goIgnoreSourceFiles = true; + rubyIgnoreSourceFiles = true; + pythonIgnoreSourceFiles = true; + cocoapodsIgnoreSourceFiles = true; + hexIgnoreSourceFiles = true; + } else { + npmIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.NPM_IGNORE_SOURCE_FILES, ConfigPropertyKeys.NPM_IGNORE_JAVA_SCRIPT_FILES, true); + bowerIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.BOWER_IGNORE_SOURCE_FILES, false); + nugetIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.NUGET_IGNORE_SOURCE_FILES, true); + mavenIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.MAVEN_IGNORE_SOURCE_FILES, false); + gradleIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.GRADLE_IGNORE_SOURCE_FILES, false); + paketIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.PAKET_IGNORE_SOURCE_FILES, ConfigPropertyKeys.PAKET_IGNORE_FILES, true); + sbtIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.SBT_IGNORE_SOURCE_FILES, false); + goIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.GO_IGNORE_SOURCE_FILES, false); + pythonIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.PYTHON_IGNORE_SOURCE_FILES, true); + rubyIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.RUBY_IGNORE_SOURCE_FILES, true); + cocoapodsIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.COCOAPODS_IGNORE_SOURCE_FILES, true); + hexIgnoreSourceFiles = config.getBooleanProperty(ConfigPropertyKeys.HEX_IGNORE_SOURCE_FILES, true); + } + + return new ResolverConfiguration(npmRunPreStep, npmResolveDependencies, npmIgnoreScripts, npmIncludeDevDependencies, npmIgnoreSourceFiles, + npmTimeoutDependenciesCollector, npmAccessToken, npmIgnoreNpmLsErrors, npmYarnProject, + bowerResolveDependencies, bowerRunPreStep, bowerIgnoreSourceFiles, + nugetResolveDependencies, nugetRestoreDependencies, nugetRunPreStep, nugetIgnoreSourceFiles, nugetResolvePakcagesConfigFiles, nugetResolveCsProjFiles, + mavenResolveDependencies, mavenIgnoredScopes, mavenAggregateModules, mavenIgnoredPomModules, mavenIgnoreSourceFiles, mavenRunPreStep, mavenIgnoreDependencyTreeErrors, + pythonResolveDependencies, pipPath, pythonPath, pythonIsWssPluginInstalled, pythonUninstallWssPluginInstalled, + pythonIgnorePipInstallErrors, pythonInstallVirtualenv, pythonResolveHierarchyTree, pythonRequirementsFileIncludes, pythonResolveSetupPyFiles, pythonIgnoreSourceFiles, + pythonIgnorePipenvInstallErrors, pythonRunPipenvPreStep, pythonInstallDevDependencies, + ignoreSourceFiles, whiteSourceConfiguration, + gradleResolveDependencies, gradleRunAssembleCommand, gradleAggregateModules, gradlePreferredEnvironment, gradleIgnoreSourceFiles, gradleRunPreStep, gradleIgnoredScopes, + graldeLocalRepositoryPath, paketResolveDependencies, paketIgnoredScopes, paketRunPreStep, paketPath, paketIgnoreSourceFiles, + goResolveDependencies, goDependencyManager, goCollectDependenciesAtRuntime, goIgnoreTestPackages, goIgnoreSourceFiles, goGradleEnableTaskAlias, + rubyResolveDependencies, rubyRunBundleInstall, rubyOverwriteGemFile, rubyInstallMissingGems, rubyIgnoreSourceFiles, + phpResolveDependencies, phpRunPreStep, phpIncludeDevDependencies, + sbtResolveDependencies, sbtAggregateModules, sbtRunPreStep, sbtTargetFolder, sbtIgnoreSourceFiles, + htmlResolveDependencies, cocoapodsResolveDependencies, cocoapodsRunPreStep, cocoapodsIgnoreSourceFiles, + hexResolveDependencies, hexRunPreStep, hexIgnoreSourceFiles, hexAggregateModules, addSha1); + } + + private RequestConfiguration getRequest(FSAConfigProperties config, String apiToken, String userKey, String projectName, String projectToken, String scanComment) { + String productToken = config.getProperty(ConfigPropertyKeys.PRODUCT_TOKEN_PROPERTY_KEY); + String productName = config.getProperty(ConfigPropertyKeys.PRODUCT_NAME_PROPERTY_KEY); + String productVersion = config.getProperty(ConfigPropertyKeys.PRODUCT_VERSION_PROPERTY_KEY); + String projectVersion = config.getProperty(ConfigPropertyKeys.PROJECT_VERSION_PROPERTY_KEY); + List appPath = (List) config.get(ConfigPropertyKeys.APP_PATH); + String iaLanguage = config.getProperty(ConfigPropertyKeys.IA_LANGUAGE, null); + String viaDebug = config.getProperty(ConfigPropertyKeys.VIA_DEBUG, EMPTY_STRING); + boolean projectPerSubFolder = config.getBooleanProperty(ConfigPropertyKeys.PROJECT_PER_SUBFOLDER, false); + String requesterEmail = config.getProperty(ConfigPropertyKeys.REQUESTER_EMAIL); + int viaAnalysis = config.getIntProperty(ConfigPropertyKeys.VIA_ANALYSIS_LEVEL, VIA_DEFAULT_ANALYSIS_LEVEL); + boolean requireKnownSha1 = config.getBooleanProperty(ConfigPropertyKeys.REQUIRE_KNOWN_SHA1, true); + return new RequestConfiguration(apiToken, userKey, requesterEmail, projectPerSubFolder, projectName, projectToken, + projectVersion, productName, productToken, productVersion, appPath, viaDebug, viaAnalysis, iaLanguage, scanComment, requireKnownSha1); + } + + private SenderConfiguration getSender(FSAConfigProperties config, String cmdServiceUrl) throws Exception { + String updateTypeValue = config.getProperty(ConfigPropertyKeys.UPDATE_TYPE, UpdateType.OVERRIDE.toString()); + boolean checkPolicies = config.getBooleanProperty(ConfigPropertyKeys.CHECK_POLICIES_PROPERTY_KEY, false); + boolean forceCheckAllDependencies = config.getBooleanProperty(ConfigPropertyKeys.FORCE_CHECK_ALL_DEPENDENCIES, false); + boolean updateInventory = config.getBooleanProperty(ConfigPropertyKeys.UPDATE_INVENTORY, true); + boolean forceUpdate = config.getBooleanProperty(ConfigPropertyKeys.FORCE_UPDATE, false); + boolean forceUpdateBuildFailed = config.getBooleanProperty(ConfigPropertyKeys.FORCE_UPDATE_FAIL_BUILD_ON_POLICY_VIOLATION, false); + boolean enableImpactAnalysis = config.getBooleanProperty(ConfigPropertyKeys.ENABLE_IMPACT_ANALYSIS, false); + String serviceUrl = cmdServiceUrl != null ? cmdServiceUrl : config.getProperty(SERVICE_URL_KEYWORD, ClientConstants.DEFAULT_SERVICE_URL); + Pattern urlPattern = Pattern.compile(SERVICE_URL_REGEX); + Matcher matcher = urlPattern.matcher(serviceUrl); + if (!matcher.find()){ + String msg = "Service URL is malformed: " + serviceUrl; + if (serviceUrl.endsWith("/agent") == false){ + msg = msg.concat(" (make sure the URL ends with '/agent')"); + } + throw new Exception(msg); + } + String proxyHost = config.getProperty(ConfigPropertyKeys.PROXY_HOST_PROPERTY_KEY); + connectionTimeOut = Integer.parseInt(config.getProperty(ClientConstants.CONNECTION_TIMEOUT_KEYWORD, + String.valueOf(ClientConstants.DEFAULT_CONNECTION_TIMEOUT_MINUTES))); + int connectionRetries = config.getIntProperty(ConfigPropertyKeys.CONNECTION_RETRIES, 1); + int connectionRetriesIntervals = config.getIntProperty(ConfigPropertyKeys.CONNECTION_RETRIES_INTERVALS, 3000); + String senderPort = config.getProperty(ConfigPropertyKeys.PROXY_PORT_PROPERTY_KEY); + + int proxyPort; + if (StringUtils.isNotEmpty(senderPort)) { + proxyPort = Integer.parseInt(senderPort); + } else { + proxyPort = -1; + } + + String proxyUser = config.getProperty(ConfigPropertyKeys.PROXY_USER_PROPERTY_KEY); + String proxyPassword = config.getProperty(ConfigPropertyKeys.PROXY_PASS_PROPERTY_KEY); + boolean ignoreCertificateCheck = config.getBooleanProperty(ConfigPropertyKeys.IGNORE_CERTIFICATE_CHECK, false); + boolean isSendLogsToWss = config.getBooleanProperty(ConfigPropertyKeys.SEND_LOGS_TO_WSS, false); + + return new SenderConfiguration(checkPolicies, serviceUrl, connectionTimeOut, + proxyHost, proxyPort, proxyUser, proxyPassword, + forceCheckAllDependencies, forceUpdate, forceUpdateBuildFailed, updateTypeValue, + enableImpactAnalysis, ignoreCertificateCheck, connectionRetries, connectionRetriesIntervals, isSendLogsToWss, updateInventory); + } + + private OfflineConfiguration getOffline(FSAConfigProperties config) { + boolean enabled = config.getBooleanProperty(ConfigPropertyKeys.OFFLINE_PROPERTY_KEY, false); + boolean zip = config.getBooleanProperty(ConfigPropertyKeys.OFFLINE_ZIP_PROPERTY_KEY, false); + boolean prettyJson = config.getBooleanProperty(ConfigPropertyKeys.OFFLINE_PRETTY_JSON_KEY, true); + String wsFolder = StringUtils.isBlank(config.getProperty(ConfigPropertyKeys.WHITESOURCE_FOLDER_PATH)) ? WHITE_SOURCE_DEFAULT_FOLDER_PATH : config.getProperty(ConfigPropertyKeys.WHITESOURCE_FOLDER_PATH); + return new OfflineConfiguration(enabled, zip, prettyJson, wsFolder); + } + + private AgentConfiguration getAgent(FSAConfigProperties config, String noConfig) { + String[] includes = Constants.TRUE.equals(noConfig) ? ExtensionUtils.INCLUDES : config.getIncludes(); + String[] excludes = config.getProperty(ConfigPropertyKeys.EXCLUDES_PATTERN_PROPERTY_KEY, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + String[] dockerIncludes = config.getDockerIncludes(); + String[] dockerExcludes = config.getProperty(ConfigPropertyKeys.DOCKER_EXCLUDES_PATTERN_PROPERTY_KEY, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + String[] projectPerFolderIncludes = config.getProjectPerFolderIncludes(); + String[] projectPerFolderExcludes = config.getProjectPerFolderExcludes(); + int archiveExtractionDepth = config.getArchiveDepth(); + String[] archiveIncludes = config.getProperty(ConfigPropertyKeys.ARCHIVE_INCLUDES_PATTERN_KEY, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + String[] archiveExcludes = config.getProperty(ConfigPropertyKeys.ARCHIVE_EXCLUDES_PATTERN_KEY, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + String[] pythonRequirementsFileIncludes = config.getPythonIncludes(); + boolean archiveFastUnpack = config.getBooleanProperty(ConfigPropertyKeys.ARCHIVE_FAST_UNPACK_KEY, false); + boolean archiveFollowSymbolicLinks = config.getBooleanProperty(ConfigPropertyKeys.FOLLOW_SYMBOLIC_LINKS, true); + boolean dockerScan = config.getBooleanProperty(ConfigPropertyKeys.SCAN_DOCKER_IMAGES, false); + boolean partialSha1Match = config.getBooleanProperty(ConfigPropertyKeys.PARTIAL_SHA1_MATCH_KEY, false); + boolean calculateHints = config.getBooleanProperty(ConfigPropertyKeys.CALCULATE_HINTS, false); + boolean calculateMd5 = config.getBooleanProperty(ConfigPropertyKeys.CALCULATE_MD5, false); + boolean showProgress = config.getBooleanProperty(ConfigPropertyKeys.SHOW_PROGRESS_BAR, true); + Pair globalCaseSensitive = getGlobalCaseSensitive(config.getProperty(ConfigPropertyKeys.CASE_SENSITIVE_GLOB_PROPERTY_KEY)); + + //key , val + + Collection excludesCopyrights = getExcludeCopyrights(config.getProperty(ConfigPropertyKeys.EXCLUDED_COPYRIGHT_KEY, EMPTY_STRING)); + + return new AgentConfiguration(includes, excludes, dockerIncludes, dockerExcludes, + archiveExtractionDepth, archiveIncludes, archiveExcludes, archiveFastUnpack, archiveFollowSymbolicLinks, + partialSha1Match, calculateHints, calculateMd5, showProgress, globalCaseSensitive.getKey(), dockerScan, excludesCopyrights, projectPerFolderIncludes, + projectPerFolderExcludes, pythonRequirementsFileIncludes, globalCaseSensitive.getValue()); + } + + private Collection getExcludeCopyrights(String excludedCopyrightsValue) { + Collection excludes = new ArrayList<>(Arrays.asList(excludedCopyrightsValue.split(Constants.COMMA))); + excludes.remove(EMPTY_STRING); + return excludes; + } + + private Pair getGlobalCaseSensitive(String globCaseSensitiveValue) { + boolean globCaseSensitive = false; + String error = null; + if (StringUtils.isNotBlank(globCaseSensitiveValue)) { + if (globCaseSensitiveValue.equalsIgnoreCase(Constants.TRUE) || globCaseSensitiveValue.equalsIgnoreCase("y")) { + globCaseSensitive = true; + error = null; + } else if (globCaseSensitiveValue.equalsIgnoreCase(Constants.FALSE) || globCaseSensitiveValue.equalsIgnoreCase("n")) { + globCaseSensitive = false; + error = null; + } else { + error = "Bad " + ConfigPropertyKeys.CASE_SENSITIVE_GLOB_PROPERTY_KEY + ". Received " + globCaseSensitiveValue + ", required true/false or y/n"; + } + } else { + error = null; + } + return new Pair<>(globCaseSensitive, error); + } + + private ScmConfiguration getScm(FSAConfigProperties config) { + String type = config.getProperty(ConfigPropertyKeys.SCM_TYPE_PROPERTY_KEY); + String url = config.getProperty(ConfigPropertyKeys.SCM_URL_PROPERTY_KEY); + String user = config.getProperty(ConfigPropertyKeys.SCM_USER_PROPERTY_KEY); + String pass = config.getProperty(ConfigPropertyKeys.SCM_PASS_PROPERTY_KEY); + String branch = config.getProperty(ConfigPropertyKeys.SCM_BRANCH_PROPERTY_KEY); + String tag = config.getProperty(ConfigPropertyKeys.SCM_TAG_PROPERTY_KEY); + String ppk = config.getProperty(ConfigPropertyKeys.SCM_PPK_PROPERTY_KEY); + + //defaults + String repositoriesPath = config.getProperty(ConfigPropertyKeys.SCM_REPOSITORIES_FILE); + boolean npmInstall = config.getBooleanProperty(ConfigPropertyKeys.SCM_NPM_INSTALL, true); + int npmInstallTimeoutMinutes = config.getIntProperty(ConfigPropertyKeys.SCM_NPM_INSTALL_TIMEOUT_MINUTES, 15); + + return new ScmConfiguration(type, user, pass, ppk, url, branch, tag, repositoriesPath, npmInstall, npmInstallTimeoutMinutes); + } + + private RemoteDockerConfiguration getRemoteDockerConfiguration(FSAConfigProperties config) { + String[] empty = new String[0]; + String[] dockerImages = config.getListProperty(ConfigPropertyKeys.DOCKER_PULL_IMAGES, null); + String[] dockerTags = config.getListProperty(ConfigPropertyKeys.DOCKER_PULL_TAGS, null); + String[] dockerDigests = config.getListProperty(ConfigPropertyKeys.DOCKER_PULL_DIGEST, null); + boolean forceDelete = config.getBooleanProperty(ConfigPropertyKeys.DOCKER_DELETE_FORCE, false); + boolean enablePulling = config.getBooleanProperty(ConfigPropertyKeys.DOCKER_PULL_ENABLE, false); + boolean loginSudo = config.getBooleanProperty(ConfigPropertyKeys.DOCKER_LOGIN_SUDO, true); + List dockerImagesList = null; + if (dockerImages != null) { + dockerImagesList = new LinkedList<>(Arrays.asList(dockerImages)); + } + List dockerTagsList = null; + if (dockerTags != null) { + dockerTagsList = new LinkedList<>(Arrays.asList(dockerTags)); + } + List dockerDigestsList = null; + if (dockerDigests != null) { + dockerDigestsList = new LinkedList<>(Arrays.asList(dockerDigests)); + } + + int maxImagesScan = config.getIntProperty(ConfigPropertyKeys.DOCKER_SCAN_MAX_IMAGES, 0); + int maxImagesPull = config.getIntProperty(ConfigPropertyKeys.DOCKER_PULL_MAX_IMAGES, 10); + boolean pullForce = config.getBooleanProperty(ConfigPropertyKeys.DOCKER_PULL_FORCE, false); + RemoteDockerConfiguration result = new RemoteDockerConfiguration(dockerImagesList, dockerTagsList, + dockerDigestsList, forceDelete, enablePulling, maxImagesScan, pullForce, maxImagesPull, loginSudo); + + // Amazon configuration + String[] dockerAmazonRegistryIds = config.getListProperty(ConfigPropertyKeys.DOCKER_AWS_REGISTRY_IDS, empty); + String dockerAmazonRegion = config.getProperty(ConfigPropertyKeys.DOCKER_AWS_REGION, "east"); + boolean enableAmazon = config.getBooleanProperty(ConfigPropertyKeys.DOCKER_AWS_ENABLE, false); + int maxPullImagesFromAmazon = config.getIntProperty(ConfigPropertyKeys.DOCKER_AWS_MAX_PULL_IMAGES, 0); + result.setAmazonRegistryIds(new LinkedList<>(Arrays.asList(dockerAmazonRegistryIds))); + result.setAmazonRegion(dockerAmazonRegion); + result.setRemoteDockerAmazonEnabled(enableAmazon); + result.setAmazonMaxPullImages(maxPullImagesFromAmazon); + + // Azure configuration + result.setRemoteDockerAzureEnabled(config.getBooleanProperty(ConfigPropertyKeys.DOCKER_AZURE_ENABLED, false)); + result.setAzureUserName(config.getProperty(ConfigPropertyKeys.DOCKER_AZURE_USER_NAME, EMPTY_STRING)); + result.setAzureUserPassword(config.getProperty(ConfigPropertyKeys.DOCKER_AZURE_USER_PASSWORD, EMPTY_STRING)); + String[] dockerAzureRegistryNames = config.getListProperty(ConfigPropertyKeys.DOCKER_AZURE_REGISTRY_NAMES, empty); + result.setAzureRegistryNames(new LinkedList<>(Arrays.asList(dockerAzureRegistryNames))); + return result; + } + + private void initializeDependencyDirsToAppPath(String[] args) { + boolean wasDir = false; + for (int i = 0; i < args.length; i++) { + if (!wasDir && args[i].equals(APP_PATH)) { + if (i + 3 < args.length && args[i + 2].equals(Constants.DASH + Constants.DIRECTORY)) { + List paths = Arrays.asList(args[i + 3].split(Constants.COMMA)); + Set value = new HashSet<>(); + value.addAll(paths); + appPathsToDependencyDirs.put(args[i + 1], value); + i = i + 3; + } else { + errors.add("Error: the '-appPath' parameter must have a following '-d'."); + return; + } + } else if (wasDir && args[i].equals(APP_PATH)) { + errors.add("Error: the '-appPath' parameter cannot follow the parameter '-d'."); + break; + } else if (args[i].equals(Constants.DASH + Constants.DIRECTORY)) { + if (i + 1 < args.length) { + if (appPathsToDependencyDirs.containsKey(DEFAULT_KEY)) { + appPathsToDependencyDirs.get(DEFAULT_KEY).addAll(Arrays.asList(args[i + 1].split(Constants.COMMA))); + } else { + List paths = Arrays.asList(args[i + 1].split(Constants.COMMA)); + Set value = new HashSet<>(); + value.addAll(paths); + appPathsToDependencyDirs.put(DEFAULT_KEY, value); + } + i++; + } else { + errors.add("Error: there is not path after the '-d' parameter."); + return; + } + wasDir = true; + } + } + if (!wasDir) { + appPathsToDependencyDirs.put(DEFAULT_KEY, new HashSet<>(dependencyDirs)); + } + } + + public static Pair> readWithError(String configFilePath, CommandLineArgs commandLineArgs) { + FSAConfigProperties configProps = new FSAConfigProperties(); + Pair> inputStreamErrorsPair = getInputStreamFromFile(configFilePath, commandLineArgs); + InputStream inputStream = inputStreamErrorsPair.getKey(); + List errors = inputStreamErrorsPair.getValue(); + + try { + // configProps.load(inputStream); replaced by the below + configProps.load(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + // Remove extra spaces from the values + Set keys = configProps.stringPropertyNames(); + for (String key : keys) { + String value = configProps.getProperty(key); + if (value != null) { + value = value.trim(); + } + configProps.put(key, value); + } + } catch (Exception e) { + errors.add("Error occurred when reading from " + configFilePath + ", error: " + e.getMessage()); + } + return new Pair<>(configProps, errors); + } + + private static Pair> getInputStreamFromFile(String filePath, CommandLineArgs commandLineArgs) { + List errors = new ArrayList<>(); + BufferedReader readFileFromUrl = null; + StringBuffer writeUrlFileContent = null; + //since we don't know if it a url or local path, so we first try to resolve a url, if it failed, then we try local path + try { + //assign the url to point to config path url + URL url = new URL(filePath); + //toDo complete proxy settings once finished in WSE-791 + Proxy proxy = null; + String[] parsedProxy = null; + String authUser = null; + String authPass = null; + URLConnection urlConnection; + + if (commandLineArgs.proxy != null) { + //get hostname, port and credentials of proxy + parsedProxy = parseProxy(commandLineArgs.proxy, errors); + authUser = parsedProxy[2]; + authPass = parsedProxy[3]; + if (parsedProxy[1] != null && Integer.valueOf(parsedProxy[1]) > 0) { + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(parsedProxy[0], Integer.valueOf(parsedProxy[1]))); + } else { + errors.add("Port must be set or greater than 0"); + } + } else if (commandLineArgs.proxyHost != null) { + authUser = commandLineArgs.proxyUser; + authPass = commandLineArgs.proxyPass; + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(commandLineArgs.proxyHost, Integer.valueOf(commandLineArgs.proxyPort))); + } + //if proxy is set, so open the connection with proxy + if (proxy != null) { + //setting proxy authenticator and disable schemes for http connections + ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator(authUser, authPass); + Authenticator.setDefault(proxyAuthenticator); + /*The 'jdk.http.auth.tunneling.disabledSchemes' property lists the authentication + schemes that will be disabled when tunneling HTTPS over a proxy, HTTP CONNECT. + so setting it to empty for this run only*/ + System.setProperty("jdk.http.auth.tunneling.disabledSchemes", EMPTY_STRING); + urlConnection = url.openConnection(proxy); + } else { + urlConnection = url.openConnection(); + } + urlConnection.connect(); + readFileFromUrl = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String inputLine; + writeUrlFileContent = new StringBuffer(); + //write data of the file to string buffer + while ((inputLine = readFileFromUrl.readLine()) != null) { + writeUrlFileContent.append(inputLine + "\n"); + } + } + //if error occurred that means it is not a url that we can resolve, still need to try local path, so do nothing in catch + catch (MalformedURLException e) { + } catch (IOException e) { + } + + InputStream inputStream = null; + //if there is any data written to the buffer, so convert to input stream + if (writeUrlFileContent != null) { + inputStream = IOUtils.toInputStream(writeUrlFileContent, UTF_8); + } else { + try { + //if string buffer still null, so try to open stream of local file path + inputStream = new FileInputStream(filePath); + } catch (FileNotFoundException e) { + errors.add("Failed to open " + filePath + " for reading " + e.getMessage()); + } + } + return new Pair<>(inputStream, errors); + } + + /* --- Public getters --- */ + + public RequestConfiguration getRequest() { + return request; + } + + public EndPointConfiguration getEndpoint() { + return endpoint; + } + + public SenderConfiguration getSender() { + return sender; + } + + public ScmConfiguration getScm() { + return scm; + } + + public AgentConfiguration getAgent() { + return agent; + } + + public OfflineConfiguration getOffline() { + return offline; + } + + public ResolverConfiguration getResolver() { + return resolver; + } + + public RemoteDockerConfiguration getRemoteDocker() { + return remoteDockerConfiguration; + } + + public String getScannedFolders() { + return scannedFolders; + } + + List getErrors() { + return errors; + } + + public List getOfflineRequestFiles() { + return offlineRequestFiles; + } + + public String getFileListPath() { + return fileListPath; + } + + public List getDependencyDirs() { + return dependencyDirs; + } + + public boolean getUseCommandLineProductName() { + return useCommandLineProductName; + } + + public boolean getUseCommandLineProjectName() { + return useCommandLineProjectName; + } + + public List getAppPaths() { + return appPaths; + } + + public Map> getAppPathsToDependencyDirs() { + return appPathsToDependencyDirs; + } + + public String getAnalyzeMultiModule() { + return analyzeMultiModule; + } + + public String getxModulePath() { + return xModulePath; + } + + public boolean isSetUpMuiltiModuleFile() { + return setUpMuiltiModuleFile; + } + + @JsonProperty(ConfigPropertyKeys.SCAN_PACKAGE_MANAGER) + public boolean isScanProjectManager() { + return scanPackageManager; + } + + @JsonProperty(ConfigPropertyKeys.SCAN_DOCKER_IMAGES) + public boolean isScanDockerImages() { + return scanDockerImages; + } + + @JsonProperty(ConfigPropertyKeys.LOG_LEVEL_KEY) + public String getLogLevel() { + return logLevel; + } + + public boolean isScanImagesTar() { + return scanTarImages; + } + + public boolean deleteTarImages() { + return deleteTarImages; + } + + /* --- Public static methods--- */ + + public static int getIntProperty(Properties config, String propertyKey, int defaultValue) { + int value = defaultValue; + String propertyValue = config.getProperty(propertyKey); + if (StringUtils.isNotBlank(propertyValue)) { + try { + value = Integer.valueOf(propertyValue); + } catch (NumberFormatException e) { + // do nothing + } + } + return value; + } + + public static boolean getBooleanProperty(Properties config, String propertyKey, boolean defaultValue) { + boolean property = defaultValue; + String propertyValue = config.getProperty(propertyKey); + if (StringUtils.isNotBlank(propertyValue)) { + property = Boolean.valueOf(propertyValue); + } + return property; + } + + public static long getLongProperty(Properties config, String propertyKey, long defaultValue) { + long property = defaultValue; + String propertyValue = config.getProperty(propertyKey); + if (StringUtils.isNotBlank(propertyValue)) { + property = Long.parseLong(propertyValue); + } + return property; + } + + public static String[] getListProperty(Properties config, String propertyName, String[] defaultValue) { + String property = config.getProperty(propertyName); + if (property == null) { + return defaultValue; + } + return property.split(Constants.WHITESPACE); + } + + public static int getArchiveDepth(Properties configProps) { + return getIntProperty(configProps, ConfigPropertyKeys.ARCHIVE_EXTRACTION_DEPTH_KEY, FSAConfiguration.DEFAULT_ARCHIVE_DEPTH); + } + + public static String[] getIncludes(Properties configProps) { + String includesString = configProps.getProperty(ConfigPropertyKeys.INCLUDES_PATTERN_PROPERTY_KEY, EMPTY_STRING); + if (StringUtils.isNotBlank(includesString)) { + return configProps.getProperty(ConfigPropertyKeys.INCLUDES_PATTERN_PROPERTY_KEY, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } + return new String[0]; + } + + private static String[] getPythonIncludes(Properties configProps) { + String includesString = configProps.getProperty(ConfigPropertyKeys.PYTHON_REQUIREMENTS_FILE_INCLUDES, Constants.PYTHON_REQUIREMENTS); + if (StringUtils.isNotBlank(includesString)) { + return configProps.getProperty(ConfigPropertyKeys.PYTHON_REQUIREMENTS_FILE_INCLUDES, Constants.PYTHON_REQUIREMENTS).split(Constants.WHITESPACE); + } + return new String[0]; + } + + public static String[] getProjectPerFolderIncludes(Properties configProps) { + String projectPerFolderIncludesString = configProps.getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_INCLUDES, null); + if (StringUtils.isNotBlank(projectPerFolderIncludesString)) { + return configProps.getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_INCLUDES, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } + if (EMPTY_STRING.equals(projectPerFolderIncludesString)) { + return null; + } + String[] result = new String[1]; + result[0] = "*"; + return result; + } + + public static String[] getProjectPerFolderExcludes(Properties configProps) { + String projectPerFolderExcludesString = configProps.getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_EXCLUDES, EMPTY_STRING); + if (StringUtils.isNotBlank(projectPerFolderExcludesString)) { + return configProps.getProperty(ConfigPropertyKeys.PROJECT_PER_FOLDER_EXCLUDES, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } + return new String[0]; + } + + public static String[] getDockerIncludes(Properties configProps) { + String includesString = configProps.getProperty(ConfigPropertyKeys.DOCKER_INCLUDES_PATTERN_PROPERTY_KEY, EMPTY_STRING); + if (StringUtils.isNotBlank(includesString)) { + return configProps.getProperty(ConfigPropertyKeys.DOCKER_INCLUDES_PATTERN_PROPERTY_KEY, EMPTY_STRING).split(FSAConfiguration.INCLUDES_EXCLUDES_SEPARATOR_REGEX); + } + return new String[0]; + } + + public List getRequirementsFileIncludes() { + return requirementsFileIncludes; + } + + public String getLogContext() { + return this.logContext; + } + + /* --- Private methods --- */ + + private List updateProperties(FSAConfigProperties configProps, CommandLineArgs commandLineArgs) { + // Check whether the user inserted api key, project OR/AND product via command line + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.ORG_TOKEN_PROPERTY_KEY, commandLineArgs.apiKey); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.UPDATE_TYPE, commandLineArgs.updateType); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PRODUCT_NAME_PROPERTY_KEY, commandLineArgs.product); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PRODUCT_VERSION_PROPERTY_KEY, commandLineArgs.productVersion); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROJECT_VERSION_PROPERTY_KEY, commandLineArgs.projectVersion); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.USER_KEY_PROPERTY_KEY, commandLineArgs.userKey); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROJECT_TOKEN_PROPERTY_KEY, commandLineArgs.projectToken); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PRODUCT_TOKEN_PROPERTY_KEY, commandLineArgs.productToken); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.LOG_LEVEL_KEY, commandLineArgs.logLevel); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.SEND_LOGS_TO_WSS, commandLineArgs.sendLogsToWss); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.SCAN_COMMENT, commandLineArgs.scanComment); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.LOG_CONTEXT, commandLineArgs.logContext); +// readPropertyFromCommandLine(configProps, SERVICE_URL_KEYWORD, commandLineArgs.wssUrl); + // request file + List offlineRequestFiles = new LinkedList<>(); + offlineRequestFiles.addAll(commandLineArgs.requestFiles); + if (offlineRequestFiles.size() > 0) { + configProps.put(ConfigPropertyKeys.OFFLINE_PROPERTY_KEY, FALSE); + } + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.OFFLINE_PROPERTY_KEY, commandLineArgs.offline); + //Impact Analysis parameters + readListFromCommandLine(configProps, ConfigPropertyKeys.APP_PATH, commandLineArgs.appPath); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.VIA_DEBUG, commandLineArgs.viaDebug); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.VIA_ANALYSIS_LEVEL, commandLineArgs.viaLevel); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.ENABLE_IMPACT_ANALYSIS, commandLineArgs.enableImpactAnalysis); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.IA_LANGUAGE, commandLineArgs.iaLanguage); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.X_PATHS, commandLineArgs.xPaths); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.ANALYZE_MULTI_MODULE, commandLineArgs.analyzeMultiModule); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.X_MODULE_PATH, commandLineArgs.xModulePath); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.ADD_SHA1, commandLineArgs.addSha1); + + // proxy + if (commandLineArgs.proxy == null) { + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_HOST_PROPERTY_KEY, commandLineArgs.proxyHost); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_PORT_PROPERTY_KEY, commandLineArgs.proxyPort); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_USER_PROPERTY_KEY, commandLineArgs.proxyUser); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_PASS_PROPERTY_KEY, commandLineArgs.proxyPass); + } else { + List errors = new ArrayList<>(); + String[] parsedProxy = parseProxy(commandLineArgs.proxy, errors); + if (errors.isEmpty()) { + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_HOST_PROPERTY_KEY, parsedProxy[0]); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_PORT_PROPERTY_KEY, parsedProxy[1]); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_USER_PROPERTY_KEY, parsedProxy[2]); + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROXY_PASS_PROPERTY_KEY, parsedProxy[3]); + } + } + + // archiving + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.ARCHIVE_FAST_UNPACK_KEY, commandLineArgs.archiveFastUnpack); + + // project per folder + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.PROJECT_PER_SUBFOLDER, commandLineArgs.projectPerFolder); + + // Check whether the user inserted scmRepositoriesFile via command line + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.SCM_REPOSITORIES_FILE, commandLineArgs.repositoriesFile); + + // User-entry of a flag that overrides default FSA process termination + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.REQUIRE_KNOWN_SHA1, commandLineArgs.requireKnownSha1); + + // docker flag to scan docker images + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.SCAN_DOCKER_IMAGES, commandLineArgs.scanDockerImages); + + //docker flag to scan docker images by using docker or tar files folder (which specified with parameter -d) + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.SCAN_TAR_IMAGES, commandLineArgs.scanDockerImages); + + //docker flag to delete tar images files after extracting + readPropertyFromCommandLine(configProps, ConfigPropertyKeys.DELETE_TAR_FILES, commandLineArgs.scanDockerImages); + + return offlineRequestFiles; + } + + //returns data of proxy url from command line parameter proxy + public static String[] parseProxy(String proxy, List errors) { + String[] parsedProxyInfo = new String[4]; + if (proxy != null) { + try { + URL proxyAsUrl = new URL(proxy); + parsedProxyInfo[0] = proxyAsUrl.getHost(); + parsedProxyInfo[1] = String.valueOf(proxyAsUrl.getPort()); + if (proxyAsUrl.getUserInfo() != null) { + String[] parsedCred = proxyAsUrl.getUserInfo().split(COLON); + parsedProxyInfo[2] = parsedCred[0]; + if (parsedCred.length > 1) { + parsedProxyInfo[3] = parsedCred[1]; + } + } + + } catch (MalformedURLException e) { + errors.add("Malformed proxy url : {}" + e.getMessage()); + } + } + return parsedProxyInfo; + } + + private void readPropertyFromCommandLine(FSAConfigProperties configProps, String propertyKey, String propertyValue) { + if (StringUtils.isNotBlank(propertyValue)) { + configProps.put(propertyKey, propertyValue); + } + } + + private void readListFromCommandLine(FSAConfigProperties configProps, String propertyKey, List propertyValue) { + if (!propertyValue.isEmpty()) { + configProps.put(propertyKey, propertyValue); + } + } + + private void commandLineArgsOverride(CommandLineArgs commandLineArgs) { + useCommandLineProductName = commandLineArgs != null && StringUtils.isNotBlank(commandLineArgs.product); + useCommandLineProjectName = commandLineArgs != null && StringUtils.isNotBlank(commandLineArgs.project); + } + + public void validate() { + getErrors().clear(); + errors.addAll(configurationValidation.getConfigurationErrors(getRequest().isProjectPerSubFolder(), getRequest().getProjectToken(), + getRequest().getProjectName(), getRequest().getApiToken(), configFilePath, getAgent().getArchiveExtractionDepth(), + getAgent().getIncludes(), getAgent().getProjectPerFolderIncludes(), getAgent().getPythonRequirementsFileIncludes(), getRequest().getScanComment())); + } + + //ProxyAuthenticator is used for proxy authentication requests + static class ProxyAuthenticator extends Authenticator { + + private String user; + private String password; + + public ProxyAuthenticator(String user, String password) { + this.user = user; + if (password != null) { + this.password = password; + } else { + this.password = EMPTY_STRING; + } + } + + //called when a request over proxy is executed + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(user, password.toCharArray()); + } + } + + @Override + public String toString() { + return "FSA Configuration {" + Constants.NEW_LINE + WsStringUtils.toString(this) + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/FileSystemAgent.java b/src/main/java/org/whitesource/fs/FileSystemAgent.java new file mode 100644 index 0000000..1c41600 --- /dev/null +++ b/src/main/java/org/whitesource/fs/FileSystemAgent.java @@ -0,0 +1,331 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.*; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.dependency.resolver.ViaMultiModuleAnalyzer; +import org.whitesource.agent.dependency.resolver.docker.DockerResolver; +import org.whitesource.agent.dependency.resolver.gradle.GradleDependencyResolver; +import org.whitesource.agent.dependency.resolver.maven.MavenDependencyResolver; +import org.whitesource.agent.dependency.resolver.npm.NpmLsJsonDependencyCollector; +import org.whitesource.agent.dependency.resolver.packageManger.PackageManagerExtractor; +import org.whitesource.agent.utils.CommandLineProcess; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.Pair; +import org.whitesource.fs.configuration.ScmConfiguration; +import org.whitesource.fs.configuration.ScmRepositoriesParser; +import org.whitesource.scm.ScmConnector; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +/** + * File System Agent. + * + * @author Itai Marko + * @author tom.shapira + * @author anna.rozin + */ +public class FileSystemAgent { + + /* --- Static members --- */ + + private Logger logger = LoggerFactory.getLogger(FileSystemAgent.class); + private static final String NPM_COMMAND = NpmLsJsonDependencyCollector.isWindows() ? "npm.cmd" : "npm"; + private static final String PACKAGE_LOCK = "package-lock.json"; + private static final String PACKAGE_JSON = "package.json"; + + /* --- Members --- */ + + private List dependencyDirs; + private final FSAConfiguration config; + + private boolean projectPerSubFolder; + + /* --- Constructors --- */ + + public FileSystemAgent(FSAConfiguration config, List dependencyDirs) { + this.config = config; + projectPerSubFolder = config.getRequest().isProjectPerSubFolder(); + if (projectPerSubFolder) { + this.dependencyDirs = new LinkedList<>(); + for (String directory : dependencyDirs) { + File file = new File(directory); + if (file.isDirectory()) { + List directories = new FilesUtils().getSubDirectories(directory, config.getAgent().getProjectPerFolderIncludes(), + config.getAgent().getProjectPerFolderExcludes(), config.getAgent().isFollowSymlinks(), config.getAgent().getGlobCaseSensitive()); + directories.forEach(subDir -> this.dependencyDirs.add(subDir.toString())); + //In case no sub-folders were found, put the top folder path as the dependencyDirs. + if (CollectionUtils.isEmpty(directories)) { + this.dependencyDirs = dependencyDirs; + } + } else if (file.isFile()) { + this.dependencyDirs.add(directory); + } else { + logger.warn("{} is not a file nor a directory .", directory); + } + } + } else { + this.dependencyDirs = dependencyDirs; + } + } + + /* --- Overridden methods --- */ + + public ProjectsDetails createProjects() { + ProjectsDetails projects = new ProjectsDetails(new ArrayList<>(), StatusCode.SUCCESS, Constants.EMPTY_STRING); + // Use FSA a as a package manger extractor for Debian/RPM/Arch Linux/Alpine + + // Check if scanPackageManager==true - This is the first priority and overrides other scans + if (config.isScanProjectManager()) { + Collection tempProjects = new PackageManagerExtractor().createProjects(); + ProjectsDetails projectsDetails = new ProjectsDetails(tempProjects, StatusCode.SUCCESS, Constants.EMPTY_STRING); + String projectName = config.getRequest().getProjectName(); + addSingleProjectToProjects(projectsDetails, projectName, projects); + return projects; + } + + // Check if docker.scanImages==true - This scans Docker Images, and should not scan any folders, so we exit + // after the scan is done + if (config.isScanDockerImages()) { + Collection tempDockerProjects = new DockerResolver(config).resolveDockerImages(); + return new ProjectsDetails(tempDockerProjects, StatusCode.SUCCESS, Constants.EMPTY_STRING); + } + + if (config.isSetUpMuiltiModuleFile()) { + ViaMultiModuleAnalyzer viaMultiModuleAnalyzer = new ViaMultiModuleAnalyzer(config.getDependencyDirs().get(0), + new MavenDependencyResolver(false, new String[]{Constants.NONE}, false, + false, false, false), Constants.TARGET, config.getAnalyzeMultiModule()); + if (viaMultiModuleAnalyzer.getBomFiles().isEmpty()) { + viaMultiModuleAnalyzer = new ViaMultiModuleAnalyzer(config.getDependencyDirs().get(0), + new GradleDependencyResolver(false, false, false, Constants.EMPTY_STRING, new String[]{Constants.NONE}, + Constants.EMPTY_STRING, false), + Constants.BUILD + File.separator + Constants.LIBS, config.getAnalyzeMultiModule()); + } + if (!viaMultiModuleAnalyzer.getBomFiles().isEmpty()) { + viaMultiModuleAnalyzer.writeFile(); + } else { + logger.error("Multi-module analysis could not establish the appPath based on the specified path. Please review the specified -d path."); + Main.exit(StatusCode.ERROR.getValue()); + } + logger.info("The multi-module analysis setup file was created successfully."); + Main.exit(StatusCode.SUCCESS.getValue()); + } + + // Scan folders and create a project per folder + if (projectPerSubFolder) { + if (this.config.getSender().isEnableImpactAnalysis()) { + logger.warn("Could not executing VIA impact analysis with the 'projectPerFolder' flag"); + return projects; + } + Map> appPathsToDependencyDirs = new HashMap<>(); + Set setDirs = new HashSet<>(1); + + for (String directory : dependencyDirs) { + setDirs.add(directory); + appPathsToDependencyDirs.put(FSAConfiguration.DEFAULT_KEY, setDirs); + + ProjectsDetails projectsDetails = getProjects(Collections.singletonList(directory), appPathsToDependencyDirs); + String projectName = new File(directory).getName(); + addSingleProjectToProjects(projectsDetails, projectName, projects); + + // return on the first project that fails + if (!projectsDetails.getStatusCode().equals(StatusCode.SUCCESS)) { + // return status code if there is a failure + return new ProjectsDetails(new ArrayList<>(), projects.getStatusCode(), projects.getDetails()); + } + appPathsToDependencyDirs.clear(); + setDirs.clear(); + } + if (CollectionUtils.isEmpty(projects.getProjects())) { + logger.warn("projectPerFolder = true, No sub-folders were found in project folder, scanning main project folder"); + projectPerSubFolder = false; + } else { + return projects; + } + } + // Scan folders and create one project for all folders together + if (!projectPerSubFolder) { // This 'if' is always true now, but keep it maybe we will do other checks in the future... + projects = getProjects(dependencyDirs, config.getAppPathsToDependencyDirs()); + if (!projects.getProjects().isEmpty()) { + AgentProjectInfo projectInfo = projects.getProjects().stream().findFirst().get(); + if (projectInfo.getCoordinates() == null) { + // use token or name + version + String projectToken = config.getRequest().getProjectToken(); + if (StringUtils.isNotBlank(projectToken)) { + projectInfo.setProjectToken(projectToken); + } else { + String projectName = config.getRequest().getProjectName(); + String projectVersion = config.getRequest().getProjectVersion(); + projectInfo.setCoordinates(new Coordinates(null, projectName, projectVersion)); + } + } + } + return projects; + } + + // todo: check for duplicates projects + return projects; + } + + /* --- Private methods --- */ + + private void addSingleProjectToProjects(ProjectsDetails projectsDetails, String projectName, ProjectsDetails projects) { + if (projectsDetails == null || projects == null || projectName == null) { + logger.debug("projectsDetails {} , projects {} , projectName {}", projectsDetails, projectName, projects); + return; + } + if (projectsDetails.getProjects().size() == 1) { + String projectVersion = config.getRequest().getProjectVersion(); + AgentProjectInfo projectInfo = projectsDetails.getProjects().stream().findFirst().get(); + projectInfo.setCoordinates(new Coordinates(null, projectName, projectVersion)); + LinkedList viaComponents = projectsDetails.getProjectToViaComponents().get(projectInfo); + projects.getProjectToViaComponents().put(projectInfo, viaComponents); + } else { + for (AgentProjectInfo projectInfo : projectsDetails.getProjects()) { + logger.debug("Project not added - {}", projectInfo); + } + } + } + + private ProjectsDetails getProjects(List scannerBaseDirs, Map> appPathsToDependencyDirs) { + // create getScm connector + final StatusCode[] success = new StatusCode[]{StatusCode.SUCCESS}; + String separatorFiles = NpmLsJsonDependencyCollector.isWindows() ? "\\" : "/"; + Collection scmPaths = new ArrayList<>(); + final boolean[] hasScmConnectors = new boolean[1]; + + List scmConnectors = null; + if (StringUtils.isNotBlank(config.getScm().getRepositoriesPath())) { + Collection scmConfigurations = new ScmRepositoriesParser().parseRepositoriesFile( + config.getScm().getRepositoriesPath(), config.getScm().getType(), config.getScm().getPpk(), config.getScm().getUser(), config.getScm().getPass()); + scmConnectors = scmConfigurations.stream() + .map(scm -> ScmConnector.create(scm.getType(), scm.getUrl(), scm.getPpk(), scm.getUser(), scm.getPass(), scm.getBranch(), scm.getTag())) + .collect(Collectors.toList()); + } else { + scmConnectors = Arrays.asList(ScmConnector.create( + config.getScm().getType(), config.getScm().getUrl(), config.getScm().getPpk(), config.getScm().getUser(), + config.getScm().getPass(), config.getScm().getBranch(), config.getScm().getTag())); + } + + if (scmConnectors != null && scmConnectors.stream().anyMatch(scm -> scm != null)) { + //scannerBaseDirs.clear(); + scmConnectors.stream().forEach(scmConnector -> { + if (scmConnector != null) { + logger.info("Connecting to SCM"); + + String scmPath = scmConnector.cloneRepository().getPath(); + Pair result = npmInstallScmRepository(config.getScm().isNpmInstall(), config.getScm().getNpmInstallTimeoutMinutes(), + scmConnector, separatorFiles, scmPath); + scmPath = result.getKey(); + success[0] = result.getValue(); + scmPaths.add(scmPath); + scannerBaseDirs.add(scmPath); + if (!appPathsToDependencyDirs.containsKey(FSAConfiguration.DEFAULT_KEY)) { + appPathsToDependencyDirs.put(FSAConfiguration.DEFAULT_KEY, new HashSet<>()); + } + appPathsToDependencyDirs.get(FSAConfiguration.DEFAULT_KEY).add(scmPath); + hasScmConnectors[0] = true; + } + }); + } + + if (StringUtils.isNotBlank(config.getAgent().getError())) { + logger.error(config.getAgent().getError()); + if (scmConnectors != null) { + scmConnectors.forEach(scmConnector -> scmConnector.deleteCloneDirectory()); + } + return new ProjectsDetails(new ArrayList<>(), StatusCode.ERROR, config.getAgent().getError()); // TODO this is within a try frame. Throw an exception instead + } + + Map> projectToAppPathAndLanguage; + ViaLanguage viaLanguage = getIaLanguage(config.getRequest().getIaLanguage()); + ProjectConfiguration projectConfiguration = new ProjectConfiguration(config.getAgent(), scannerBaseDirs, appPathsToDependencyDirs, false); + projectToAppPathAndLanguage = new FileSystemScanner(config.getResolver(), config.getAgent() , config.getSender().isEnableImpactAnalysis(), viaLanguage) + .createProjects(projectConfiguration); + ProjectsDetails projectsDetails = new ProjectsDetails(projectToAppPathAndLanguage, success[0], Constants.EMPTY_STRING); + + // delete all temp scm files + scmPaths.forEach(directory -> { + if (directory != null) { + try { + FileUtils.forceDelete(new File(directory)); + } catch (IOException e) { + // do nothing + } + } + }); + return projectsDetails; + } + + private ViaLanguage getIaLanguage(String iaLanguage) { + ViaLanguage[] values = ViaLanguage.values(); + if (iaLanguage != null) { + for (ViaLanguage value : values) { + if (value.toString().toLowerCase().equals(iaLanguage.toLowerCase())) { + return value; + } + } + } + return null; + } + + private Pair npmInstallScmRepository(boolean scmNpmInstall, int npmInstallTimeoutMinutes, ScmConnector scmConnector, + String separatorFiles, String pathToCloneRepoFiles) { + + StatusCode success = StatusCode.SUCCESS; + File packageJson = new File(pathToCloneRepoFiles + separatorFiles + PACKAGE_JSON); + boolean npmInstallFailed = false; + if (scmNpmInstall && packageJson.exists()) { + // execute 'npm install' + File packageLock = new File(pathToCloneRepoFiles + separatorFiles + PACKAGE_LOCK); + if (packageLock.exists()) { + packageLock.delete(); + } + CommandLineProcess npmInstall = new CommandLineProcess(pathToCloneRepoFiles, new String[]{NPM_COMMAND, Constants.INSTALL}); + logger.info("Found package.json file, executing 'npm install' on {}", scmConnector.getUrl()); + try { + npmInstall.executeProcessWithoutOutput(); + npmInstall.setTimeoutProcessMinutes(npmInstallTimeoutMinutes); + if (npmInstall.isErrorInProcess()) { + npmInstallFailed = true; + logger.error("Failed to run 'npm install' on {}", scmConnector.getUrl()); + } + } catch (IOException e) { + npmInstallFailed = true; + logger.error("Failed to start 'npm install', Please make sure 'npm' is installed. {}", e.getMessage()); + logger.debug("Failed to run 'npm install' command ", e); + } + if (npmInstallFailed) { + // In case of error in 'npm install', delete and clone the repository to prevent wrong output + success = StatusCode.PRE_STEP_FAILURE; + scmConnector.deleteCloneDirectory(); + pathToCloneRepoFiles = scmConnector.cloneRepository().getPath(); + } + } + return new Pair<>(pathToCloneRepoFiles, success); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/FileSystemAgentInfo.java b/src/main/java/org/whitesource/fs/FileSystemAgentInfo.java new file mode 100644 index 0000000..548cc94 --- /dev/null +++ b/src/main/java/org/whitesource/fs/FileSystemAgentInfo.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.contracts.PluginInfo; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class FileSystemAgentInfo implements PluginInfo { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(FileSystemAgentInfo.class); + private static final String AGENT_TYPE = "fs-agent"; + private static final String AGENTS_VERSION = "agentsVersion"; + + /* --- Members --- */ + + private Properties artifactProperties; + + /* --- Constructor --- */ + + public FileSystemAgentInfo() { + this.artifactProperties = getArtifactProperties(); + } + + /* --- Getters --- */ + + @Override + public String getAgentType() { + return AGENT_TYPE; + } + + @Override + public String getAgentVersion() { + return getResource(AGENTS_VERSION); + } + + @Override + public String getPluginVersion() { + return getResource(Constants.VERSION); + } + + private String getResource(String propertyName) { + String val = (artifactProperties.getProperty(propertyName)); + if (StringUtils.isNotBlank(val)) { + return val; + } + return Constants.EMPTY_STRING; + } + + /* --- Private members --- */ + + private Properties getArtifactProperties() { + Properties properties = new Properties(); + try (InputStream stream = Main.class.getResourceAsStream("/project.properties")) { + properties.load(stream); + } catch (IOException e) { + logger.error("Failed to get version ", e); + } + return properties; + } +} diff --git a/src/main/java/org/whitesource/fs/LogMapAppender.java b/src/main/java/org/whitesource/fs/LogMapAppender.java new file mode 100644 index 0000000..6e6c655 --- /dev/null +++ b/src/main/java/org/whitesource/fs/LogMapAppender.java @@ -0,0 +1,37 @@ +package org.whitesource.fs; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import org.slf4j.LoggerFactory; +import org.whitesource.agent.Constants; +import java.util.concurrent.ConcurrentSkipListMap; + +public class LogMapAppender extends AppenderBase { + + // using this collection as its thread-safe and easily sortable + private ConcurrentSkipListMap logEvents = new ConcurrentSkipListMap<>(); + private Level rootLevel; + + @Override + protected void append(ILoggingEvent iLoggingEvent) { + // it is possible that multiple events have the same time-stamp. in such case - increment the time-stamp key + long timeStamp = iLoggingEvent.getTimeStamp(); + while (logEvents.get(timeStamp) != null){ + timeStamp++; + } + logEvents.put(timeStamp, iLoggingEvent); + ch.qos.logback.classic.Logger logsSet = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Constants.MAP_LOG_NAME); + // by setting the 'additive' property of this logger dynamically, it allows the pass the incoming event to the + // parent logger depending on the event's level and root's level + logsSet.setAdditive(iLoggingEvent.getLevel().levelInt >= rootLevel.levelInt); + } + + public ConcurrentSkipListMap getLogEvents(){ + return logEvents; + } + + public void setRootLevel(Level rootLevel) { + this.rootLevel = rootLevel; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/LogMapDefiner.java b/src/main/java/org/whitesource/fs/LogMapDefiner.java new file mode 100644 index 0000000..82e2835 --- /dev/null +++ b/src/main/java/org/whitesource/fs/LogMapDefiner.java @@ -0,0 +1,31 @@ +package org.whitesource.fs; + +import ch.qos.logback.core.PropertyDefinerBase; +import org.whitesource.agent.Constants; + +import java.util.HashMap; +import java.util.Map; + +public class LogMapDefiner extends PropertyDefinerBase { + + /* --- Static members --- */ + private static Map properties = new HashMap<>(); + protected static final String APPENDER_NAME = "appenderName"; + protected static final String LOGGER_NAME = "loggerName"; + + static { + properties.put(APPENDER_NAME, Constants.MAP_APPENDER_NAME); + properties.put(LOGGER_NAME, Constants.MAP_LOG_NAME); + } + + private String propertyLookupKey; + + public void setPropertyLookupKey(String propertyLookupKey) { + this.propertyLookupKey = propertyLookupKey; + } + + @Override + public String getPropertyValue() { + return properties.get(propertyLookupKey); + } +} diff --git a/src/main/java/org/whitesource/fs/Main.java b/src/main/java/org/whitesource/fs/Main.java new file mode 100644 index 0000000..a979dd8 --- /dev/null +++ b/src/main/java/org/whitesource/fs/Main.java @@ -0,0 +1,292 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.util.ContextInitializer; +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.core.json.JsonObject; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.ProjectsSender; +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.api.dispatch.UpdateInventoryRequest; +import org.whitesource.agent.api.model.AgentProjectInfo; +import org.whitesource.agent.api.model.Coordinates; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.utils.Pair; +import org.whitesource.fs.configuration.ConfigurationSerializer; +import org.whitesource.fs.configuration.RequestConfiguration; +import org.whitesource.web.FsaVerticle; + +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Author: Itai Marko + */ +public class Main { + protected static final String LOGBACK_FSA_XML = "logback-FSA.xml"; + + /* --- Static members --- */ + + public static Logger logger; // don't initialize the logger here, only after setting the + // ContextInitializer.CONFIG_FILE_PROPERTY property (set inside setLoggerConfiguration method) + public static final long MAX_TIMEOUT = 1000 * 60 * 60; + private static ProjectsSender projectsSender = null; + private static Vertx vertx; + public static int exitCode = 0; + + ProjectsCalculator projectsCalculator = new ProjectsCalculator(); + public static final String HELP_CONTENT_FILE_NAME = "helpContent.txt"; + + /* --- Main --- */ + + public static void main(String[] args) { + int exitCode = mainScan(args); + exit(exitCode); + } + + private static int mainScan(String[] args) { + + if (isHelpArg(args)) { + printHelpContent(); + System.exit(StatusCode.SUCCESS.getValue()); + } + + CommandLineArgs commandLineArgs = new CommandLineArgs(); + commandLineArgs.parseCommandLine(args); + + StatusCode processExitCode; + + // read configuration senderConfig + FSAConfiguration fsaConfiguration = new FSAConfiguration(args); + // don't make any reference to the logger before calling this method + + setLoggerConfiguration(fsaConfiguration.getLogLevel(), fsaConfiguration.getLogContext()); + + boolean isStandalone = commandLineArgs.web.equals(Constants.FALSE); + logger.info(fsaConfiguration.toString()); + if (fsaConfiguration.getSender() != null && fsaConfiguration.getSender().isSendLogsToWss()) { + logger.info("-----------------------------------------------------------------------------"); + logger.info("'sendLogsToWss' parameter is enabled"); + logger.info("Data of your scan will be sent to WhiteSource for diagnostic purposes"); + logger.info("-----------------------------------------------------------------------------"); + } + if (isStandalone) { + try { + if (fsaConfiguration.getErrors() == null || fsaConfiguration.getErrors().size() > 0) { + processExitCode = StatusCode.ERROR; + fsaConfiguration.getErrors().forEach(error -> logger.error(error)); + logger.warn("Exiting"); + } else { + processExitCode = new Main().scanAndSend(fsaConfiguration, true).getStatusCode(); + } + } catch (Exception e) { + // catch any exception that may be thrown, return error code + logger.warn("Process encountered an error: {}" + e.getMessage(), e); + processExitCode = StatusCode.ERROR; + } finally { + new TempFolders().deleteTempFolders(); + } + + logger.info("Process finished with exit code {} ({})", processExitCode.name(), processExitCode.getValue()); + exitCode = getValue(processExitCode); + } else { + //this is a work around + vertx = Vertx.vertx(new VertxOptions() + .setBlockedThreadCheckInterval(MAX_TIMEOUT)); + + JsonObject config = new JsonObject(); + config.put(FsaVerticle.CONFIGURATION, new ConfigurationSerializer().getAsString(fsaConfiguration, false)); + DeploymentOptions options = new DeploymentOptions() + .setConfig(config) + .setWorker(true); + vertx.deployVerticle(FsaVerticle.class.getName(), options); + } + return exitCode; + } + + private static int getValue(StatusCode processExitCode) { + return processExitCode.getValue(); + } + + private static void setLoggerConfiguration(String logLevel, String logContext) { + // setting the logback name manually, to override the default logback.xml which is originated from the jar of wss-agent-api-client. + // making sure this is done before initializing the logger object, for otherwise this overriding will fail + System.setProperty(ContextInitializer.CONFIG_FILE_PROPERTY, LOGBACK_FSA_XML); + if (StringUtils.isNotEmpty(logContext)) { + LoggerFactory.contextId = logContext; + } + logger = LoggerFactory.getLogger(Main.class); + // read log level from configuration file + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + ch.qos.logback.classic.Logger mapLog = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger(Constants.MAP_LOG_NAME); + root.setLevel(Level.toLevel(logLevel, Level.INFO)); + ((LogMapAppender) mapLog.getAppender(Constants.MAP_APPENDER_NAME)).setRootLevel(root.getLevel()); + } + + public ProjectsDetails scanAndSend(FSAConfiguration fsaConfiguration, boolean shouldSend) { + if (fsaConfiguration.getErrors() != null && fsaConfiguration.getErrors().size() > 0) { + return new ProjectsDetails(new ArrayList<>(), StatusCode.ERROR, String.join(System.lineSeparator(), fsaConfiguration.getErrors())); + } + + ProjectsDetails result = projectsCalculator.getAllProjects(fsaConfiguration); + + OfflineReader offlineReader = new OfflineReader(); + Collection updateInventoryRequests = offlineReader.getAgentProjectsFromRequests(fsaConfiguration.getOfflineRequestFiles()); + Collection offlineProjects = updateInventoryRequests.stream().flatMap(updateInventoryRequest -> + updateInventoryRequest.getProjects().stream()).collect(Collectors.toList()); + + if (fsaConfiguration.getOfflineRequestFiles() != null) { + result.addOfflineProjects(offlineProjects); + } else { + // in case of offline requests remove other + } + + if (fsaConfiguration.getUseCommandLineProjectName()) { + // change project name from command line in case the user sent name via commandLine + String projectName = fsaConfiguration.getRequest().getProjectName(); + + Set agentProjectInfos = new HashSet<>(); + for (AgentProjectInfo projectInfo : result.getProjectToViaComponents().keySet()) { + agentProjectInfos.add(projectInfo); + } + if (agentProjectInfos.size() == 1 && projectName != null) { + for (AgentProjectInfo project : agentProjectInfos) { + project.getCoordinates().setArtifactId(projectName); + } + } + } + + RequestConfiguration req = fsaConfiguration.getRequest(); + // updating the product name and version from the offline file + if (fsaConfiguration != null && !fsaConfiguration.getUseCommandLineProductName() && updateInventoryRequests.size() > 0) { + UpdateInventoryRequest offLineReq = updateInventoryRequests.stream().findFirst().get(); + req = new RequestConfiguration(req.getApiToken(), req.getUserKey(), req.getRequesterEmail(), req.isProjectPerSubFolder(), req.getProjectName(), + req.getProjectToken(), req.getProjectVersion(), offLineReq.product(), null, offLineReq.productVersion(), + req.getAppPaths(), req.getViaDebug(), req.getViaAnalysisLevel(), req.getIaLanguage(), req.getScanComment(), req.isRequireKnownSha1()); + } + + if (!result.getStatusCode().equals(StatusCode.SUCCESS)) { + return new ProjectsDetails(result.getProjects(), result.getStatusCode(), Constants.EMPTY_STRING); + } + + if (shouldSend) { + ProjectsSender projectsSender = getProjectsSender(fsaConfiguration, req); + Pair processExitCode = sendProjects(projectsSender, result); + logger.debug("Process finished with exit code {} ({})", processExitCode.getKey(), processExitCode.getValue()); + return new ProjectsDetails(new ArrayList<>(), processExitCode.getValue(), processExitCode.getKey()); + } else { + return new ProjectsDetails(result.getProjects(), result.getStatusCode(), Constants.EMPTY_STRING); + } + } + + private ProjectsSender getProjectsSender(FSAConfiguration fsaConfiguration, RequestConfiguration req) { + ProjectsSender projectsSender; + if (!projectSenderExist()) { + projectsSender = new ProjectsSender(fsaConfiguration.getSender(), fsaConfiguration.getOffline(), req, new FileSystemAgentInfo()); + } else { + projectsSender = Main.projectsSender; + } + return projectsSender; + } + + private Pair sendProjects(ProjectsSender projectsSender, ProjectsDetails projectsDetails) { + Collection projects = projectsDetails.getProjects(); + Iterator iterator = projects.iterator(); + while (iterator.hasNext()) { + AgentProjectInfo project = iterator.next(); + if (project.getDependencies().isEmpty()) { + iterator.remove(); + + // if coordinates are null, then use token + String projectIdentifier; + Coordinates coordinates = project.getCoordinates(); + if (coordinates == null) { + projectIdentifier = project.getProjectToken(); + } else { + projectIdentifier = coordinates.getArtifactId(); + } + logger.info("Removing empty project {} from update (found 0 matching files)", projectIdentifier); + } + } + + if (projects.isEmpty()) { + logger.info("Exiting, nothing to update"); + return new Pair<>("Exiting, nothing to update", StatusCode.SUCCESS); + } else { + return projectsSender.sendRequest(projectsDetails);//todo + } + } + + private static boolean isHelpArg(String[] args) { + for (String arg : args) { + if (Constants.HELP_ARG1.equals(arg) || Constants.HELP_ARG2.equals(arg)) { + return true; + } + } + return false; + } + + private static void printHelpContent() { + logger = LoggerFactory.getLogger(Main.class); + InputStream inputStream = null; + BufferedReader bufferedReader = null; + try { + ClassLoader classLoader = Main.class.getClassLoader(); + inputStream = classLoader.getResourceAsStream(HELP_CONTENT_FILE_NAME); + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + String result = ""; + String line = bufferedReader.readLine(); + while (line != null) { + result = result + line + System.lineSeparator(); + line = bufferedReader.readLine(); + } + logger.info(result); + } catch (IOException e) { + logger.warn("Could not show the help command"); + } + try { + if (inputStream != null) { + inputStream.close(); + } + if (bufferedReader != null) { + bufferedReader.close(); + } + } catch (IOException e) { + logger.warn("Could not close the help file"); + } + } + + public static void exit(int statusCode) { + System.exit(statusCode); + } + + private boolean projectSenderExist() { + return Main.projectsSender != null; + } + + // end to end integration projectSenderExist + protected static void endToEndIntegration(String[] args, ProjectsSender testProjectsSender) { + projectsSender = testProjectsSender; + mainScan(args); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/OfflineReader.java b/src/main/java/org/whitesource/fs/OfflineReader.java new file mode 100644 index 0000000..36a99e7 --- /dev/null +++ b/src/main/java/org/whitesource/fs/OfflineReader.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.api.dispatch.UpdateInventoryRequest; + +import java.io.*; +import java.util.Base64; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; + +public class OfflineReader { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(OfflineReader.class); + private static final String UTF_8 = "UTF-8"; + + public Collection getAgentProjectsFromRequests(List offlineRequestFiles){//, FSAConfiguration fsaConfiguration) { + Collection projects = new LinkedList<>(); + + List requestFiles = new LinkedList<>(); + if (offlineRequestFiles != null) { + for (String requestFilePath : offlineRequestFiles) { + if (StringUtils.isNotBlank(requestFilePath)) { + requestFiles.add(new File(requestFilePath)); + } + } + } + if (!requestFiles.isEmpty()) { + for (File requestFile : requestFiles) { + if (!requestFile.isFile()) { + logger.warn("'{}' is a folder. Enter a valid file path, folder is not acceptable.", requestFile.getName()); + continue; + } + Gson gson = new Gson(); + UpdateInventoryRequest updateRequest; + logger.debug("Converting offline request to JSON"); + try(FileReader fileReader = new FileReader(requestFile); + JsonReader jsonReader = new JsonReader(fileReader)){ + updateRequest = gson.fromJson(jsonReader, new TypeToken() { + }.getType()); + logger.info("Reading information from request file {}", requestFile); + projects.add(updateRequest); + } catch (JsonSyntaxException e) { + // try to decompress file content + try { + logger.debug("Decompressing zipped offline request"); + String fileContent = decompress(requestFile); + logger.debug("Converting offline request to JSON"); + updateRequest = gson.fromJson(fileContent, new TypeToken() {}.getType()); + logger.info("Reading information from request file {}", requestFile); + projects.add(updateRequest); + } catch (IOException ioe) { + logger.warn("Error parsing request: " + ioe.getMessage()); + } catch (JsonSyntaxException jse) { + logger.warn("Error parsing request: " + jse.getMessage()); + } + } catch (FileNotFoundException e) { + logger.warn("Error parsing request: " + e.getMessage()); + } catch (IOException e) { + logger.warn("Error parsing request: " + e.getMessage()); + } + } + } + return projects; + } + + /* --- Private methods --- */ + + private static String decompress(File file) throws IOException { + if (file == null || !file.exists()) { + return Constants.EMPTY_STRING; + } + + byte[] bytes = Base64.getDecoder().decode(IOUtils.toByteArray(new FileInputStream(file))); + GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(bytes)); + BufferedReader bf = new BufferedReader(new InputStreamReader(gzipInputStream, UTF_8)); + StringBuilder outStr = new StringBuilder(Constants.EMPTY_STRING); + String line; + while ((line = bf.readLine()) != null) { + outStr.append(line); + } + return outStr.toString(); + } +} diff --git a/src/main/java/org/whitesource/fs/ProjectsCalculator.java b/src/main/java/org/whitesource/fs/ProjectsCalculator.java new file mode 100644 index 0000000..11ef5d2 --- /dev/null +++ b/src/main/java/org/whitesource/fs/ProjectsCalculator.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import ch.qos.logback.classic.Level; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ProjectsCalculator { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(ProjectsCalculator.class); + + /* --- Public methods --- */ + + public ProjectsDetails getAllProjects(FSAConfiguration fsaConfiguration) { + + // read directories and files from list-file + List files = new ArrayList<>(); + if (StringUtils.isNotBlank(fsaConfiguration.getFileListPath())) { + try { + File listFile = new File(fsaConfiguration.getFileListPath()); + if (listFile.exists()) { + files.addAll(FileUtils.readLines(listFile)); + } + } catch (IOException e) { + logger.warn("Error reading list file"); + } + } + + // read csv directory list + files.addAll(fsaConfiguration.getDependencyDirs()); + + // add directory list to appPath map - defaultKey + fsaConfiguration.getAppPathsToDependencyDirs().get(FSAConfiguration.DEFAULT_KEY).addAll(files); + + // run the agent + FileSystemAgent agent = new FileSystemAgent(fsaConfiguration, files); + // create projects as usual + return agent.createProjects(); + } +} diff --git a/src/main/java/org/whitesource/fs/ProjectsDetails.java b/src/main/java/org/whitesource/fs/ProjectsDetails.java new file mode 100644 index 0000000..45fedfe --- /dev/null +++ b/src/main/java/org/whitesource/fs/ProjectsDetails.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +import org.whitesource.agent.ViaComponents; +import org.whitesource.agent.api.model.AgentProjectInfo; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +public class ProjectsDetails { + +// private Collection projects; + private Map> projectToViaComponents; + private String details; + private StatusCode statusCode; + + + public ProjectsDetails(Map> projectToViaComponents, StatusCode statusCode , String details) { + this.statusCode = statusCode; + this.projectToViaComponents = projectToViaComponents; + this.details = details; + } + + public ProjectsDetails(Collection projects, StatusCode statusCode , String details) { + Map> projectToAppPathAndLanguage = new HashMap<>(); + for (AgentProjectInfo project : projects) { + projectToAppPathAndLanguage.put(project, new LinkedList<>()); + } + this.statusCode = statusCode; + this.projectToViaComponents = projectToAppPathAndLanguage; + this.details = details; + } + + public ProjectsDetails() { + } + + public StatusCode getStatusCode() { + return statusCode; + } + + public String getDetails() { + return details; + } + + public Map> getProjectToViaComponents() { + return projectToViaComponents; + } + + public void setProjectToViaComponents(Map> projectToViaComponents) { + this.projectToViaComponents = projectToViaComponents; + } + + public void addOfflineProjects(Collection projects) { + if (projects.size() > 0){ + for (AgentProjectInfo agentProjectInfo : projects) { + getProjectToViaComponents().put(agentProjectInfo, null); + } + } + } + + public Collection getProjects() { + return getProjectToViaComponents().keySet(); + } +} diff --git a/src/main/java/org/whitesource/fs/StatusCode.java b/src/main/java/org/whitesource/fs/StatusCode.java new file mode 100644 index 0000000..f0f6a02 --- /dev/null +++ b/src/main/java/org/whitesource/fs/StatusCode.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2017 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs; + +/** + *@author eugen.horovitz + */ +public enum StatusCode { + + SUCCESS(0), ERROR(-1), POLICY_VIOLATION(-2), CLIENT_FAILURE(-3), CONNECTION_FAILURE(-4), SERVER_FAILURE(-5), PRE_STEP_FAILURE(-6); + + private final int value; + + StatusCode(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/WsSecret.java b/src/main/java/org/whitesource/fs/WsSecret.java new file mode 100644 index 0000000..8db8b54 --- /dev/null +++ b/src/main/java/org/whitesource/fs/WsSecret.java @@ -0,0 +1,12 @@ +package org.whitesource.fs; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface WsSecret { + public String value() default "******"; +} diff --git a/src/main/java/org/whitesource/fs/configuration/AgentConfiguration.java b/src/main/java/org/whitesource/fs/configuration/AgentConfiguration.java new file mode 100644 index 0000000..870f789 --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/AgentConfiguration.java @@ -0,0 +1,212 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.whitesource.agent.utils.WsStringUtils; +import org.whitesource.fs.FSAConfigProperty; + +import java.util.Collection; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +public class AgentConfiguration { + + public static final String ERROR = "error"; + @FSAConfigProperty + private final String[] includes; + @FSAConfigProperty + private final String[] excludes; + @FSAConfigProperty + private final String[] dockerIncludes; + @FSAConfigProperty + private final String[] dockerExcludes; + @FSAConfigProperty + private final String[] pythonRequirementsFileIncludes; + @FSAConfigProperty + private final int archiveExtractionDepth; + @FSAConfigProperty + private final String[] archiveIncludes; + @FSAConfigProperty + private final String[] archiveExcludes; + private final boolean archiveFastUnpack; + @FSAConfigProperty + private final boolean followSymlinks; + private final boolean partialSha1Match; + private final boolean calculateHints; + private final boolean calculateMd5; + @FSAConfigProperty + private final boolean dockerScan; + private final boolean showProgressBar; + @FSAConfigProperty + private final boolean globCaseSensitive; + private final Collection excludedCopyrights; + @FSAConfigProperty + private final String[] projectPerFolderIncludes; + @FSAConfigProperty + private final String[] projectPerFolderExcludes; + private final String error; + + @JsonProperty(ERROR) + public String getError() { + return error; + } + + @JsonCreator + public AgentConfiguration(@JsonProperty(INCLUDES_PATTERN_PROPERTY_KEY) String[] includes, + @JsonProperty(EXCLUDES_PATTERN_PROPERTY_KEY) String[] excludes, + @JsonProperty(DOCKER_INCLUDES_PATTERN_PROPERTY_KEY) String[] dockerIncludes, + @JsonProperty(DOCKER_EXCLUDES_PATTERN_PROPERTY_KEY) String[] dockerExcludes, + @JsonProperty(ARCHIVE_EXTRACTION_DEPTH_KEY) int archiveExtractionDepth, + @JsonProperty(ARCHIVE_INCLUDES_PATTERN_KEY) String[] archiveIncludes, + @JsonProperty(ARCHIVE_EXCLUDES_PATTERN_KEY) String[] archiveExcludes, + @JsonProperty(ARCHIVE_FAST_UNPACK_KEY) boolean archiveFastUnpack, + @JsonProperty(FOLLOW_SYMBOLIC_LINKS) boolean followSymlinks, + @JsonProperty(PARTIAL_SHA1_MATCH_KEY) boolean partialSha1Match, + @JsonProperty(CALCULATE_HINTS) boolean calculateHints, + @JsonProperty(CALCULATE_MD5) boolean calculateMd5, + @JsonProperty(SHOW_PROGRESS_BAR) boolean showProgressBar, + @JsonProperty(CASE_SENSITIVE_GLOB_PROPERTY_KEY) boolean globCaseSensitive, + @JsonProperty(SCAN_DOCKER_IMAGES) boolean dockerScan, + @JsonProperty(EXCLUDED_COPYRIGHT_KEY) Collection excludedCopyrights, + @JsonProperty(PROJECT_PER_FOLDER_INCLUDES) String[] projectPerFolderIncludes, + @JsonProperty(PROJECT_PER_FOLDER_EXCLUDES) String[] projectPerFolderExcludes, + @JsonProperty(PYTHON_REQUIREMENTS_FILE_INCLUDES) String[] pythonRequirementsFileIncludes, + @JsonProperty(ERROR) String error) { + this.includes = includes == null ? new String[0] : includes; + this.excludes = excludes == null ? new String[0] : excludes; + this.dockerIncludes = dockerIncludes == null ? new String[0] : dockerIncludes; + this.dockerExcludes = dockerExcludes == null ? new String[0] : dockerExcludes; + this.archiveExtractionDepth = archiveExtractionDepth; + this.archiveIncludes = archiveIncludes == null ? new String[0] : archiveIncludes; + this.archiveExcludes = archiveExcludes == null ? new String[0] : archiveExcludes; + this.archiveFastUnpack = archiveFastUnpack; + this.followSymlinks = followSymlinks; + this.dockerScan = dockerScan; + this.partialSha1Match = partialSha1Match; + this.calculateHints = calculateHints; + this.calculateMd5 = calculateMd5; + this.showProgressBar = showProgressBar; + this.globCaseSensitive = globCaseSensitive; + this.error = error; + this.excludedCopyrights = excludedCopyrights; + this.projectPerFolderIncludes = projectPerFolderIncludes; + this.projectPerFolderExcludes = projectPerFolderExcludes; + this.pythonRequirementsFileIncludes = pythonRequirementsFileIncludes == null ? new String[0] : pythonRequirementsFileIncludes; + } + + @JsonProperty(SHOW_PROGRESS_BAR) + public boolean isShowProgressBar() { + return showProgressBar; + } + + @JsonProperty(EXCLUDED_COPYRIGHT_KEY) + public Collection getExcludedCopyrights() { + return excludedCopyrights; + } + + @JsonProperty(CASE_SENSITIVE_GLOB_PROPERTY_KEY) + public boolean getGlobCaseSensitive() { + return globCaseSensitive; + } + + @JsonProperty(INCLUDES_PATTERN_PROPERTY_KEY) + public String[] getIncludes() { + return includes; + } + + @JsonProperty(EXCLUDES_PATTERN_PROPERTY_KEY) + public String[] getExcludes() { + return excludes; + } + + @JsonProperty(ARCHIVE_EXTRACTION_DEPTH_KEY) + public int getArchiveExtractionDepth() { + return archiveExtractionDepth; + } + + @JsonProperty(ARCHIVE_INCLUDES_PATTERN_KEY) + public String[] getArchiveIncludes() { + return archiveIncludes; + } + + @JsonProperty(ARCHIVE_EXCLUDES_PATTERN_KEY) + public String[] getArchiveExcludes() { + return archiveExcludes; + } + + @JsonProperty(PYTHON_REQUIREMENTS_FILE_INCLUDES) + public String[] getPythonRequirementsFileIncludes() { + return pythonRequirementsFileIncludes; + } + + @JsonProperty(ARCHIVE_FAST_UNPACK_KEY) + public boolean isArchiveFastUnpack() { + return archiveFastUnpack; + } + + @JsonProperty(FOLLOW_SYMBOLIC_LINKS) + public boolean isFollowSymlinks() { + return followSymlinks; + } + + @JsonProperty(PARTIAL_SHA1_MATCH_KEY) + public boolean isPartialSha1Match() { + return partialSha1Match; + } + + @JsonProperty(CALCULATE_HINTS) + public boolean isCalculateHints() { + return calculateHints; + } + + @JsonProperty(CALCULATE_MD5) + public boolean isCalculateMd5() { + return calculateMd5; + } + + @JsonProperty(DOCKER_INCLUDES_PATTERN_PROPERTY_KEY) + public String[] getDockerIncludes() { + return dockerIncludes; + } + + @JsonProperty(DOCKER_EXCLUDES_PATTERN_PROPERTY_KEY) + public String[] getDockerExcludes() { + return dockerExcludes; + } + + @JsonProperty(SCAN_DOCKER_IMAGES) + public boolean isDockerScan() { + return dockerScan; + } + + @JsonProperty(PROJECT_PER_FOLDER_INCLUDES) + public String[] getProjectPerFolderIncludes() { + return projectPerFolderIncludes; + } + + @JsonProperty(PROJECT_PER_FOLDER_EXCLUDES) + public String[] getProjectPerFolderExcludes() { + return projectPerFolderExcludes; + } + + + @Override + public String toString() { + return WsStringUtils.toString(this); + } +} diff --git a/src/main/java/org/whitesource/fs/configuration/ConfigurationSerializer.java b/src/main/java/org/whitesource/fs/configuration/ConfigurationSerializer.java new file mode 100644 index 0000000..a253166 --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/ConfigurationSerializer.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.fs.FSAConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.Properties; + +public class ConfigurationSerializer { + + /* --- Static members --- */ + + private static final Logger logger = LoggerFactory.getLogger(ConfigurationSerializer.class); + + /* --- Members --- */ + + private static ObjectMapper jsonMapper = new ObjectMapper(); + private static ObjectMapper jsonMapperWithoutNulls = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); + private static ObjectMapper yamlMapperWithoutNulls = new ObjectMapper(new YAMLFactory().enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + /* --- Constructors --- */ + + public FSAConfiguration load(String fileInput) throws IOException { + //todo: add support to load from properties file + FSAConfiguration config = jsonMapper.readValue(new File(fileInput), FSAConfiguration.class); + return config; + } + + public boolean save(T object, String fileOutput, boolean includeNulls) { + try { + if (includeNulls) { + jsonMapper.writeValue(new File(fileOutput), object); + } else { + jsonMapperWithoutNulls.writeValue(new File(fileOutput), object); + } + return true; + } catch (IOException e) { + logger.error("error saving configuration ", e.getStackTrace()); + return false; + } + } + + public boolean saveYaml(T object, String fileOutput) { + try { + yamlMapperWithoutNulls.writeValue(new File(fileOutput), object); + return true; + } catch (IOException e) { + logger.error("error saving configuration ", e.getStackTrace()); + return false; + } + } + + public String getAsString(T object, boolean includeNulls) { + try { + if (includeNulls) { + return jsonMapperWithoutNulls.writeValueAsString(object); + } else { + return jsonMapperWithoutNulls.writeValueAsString(object); + } + } catch (IOException e) { + logger.error("error getting configuration ", e.getStackTrace()); + return null; + } + } + + public static Properties getAsProperties(T object) { + Map map = jsonMapper.convertValue(object, Map.class); + Properties properties = new Properties(); + fillProperties(map, properties); + return properties; + } + + private static void fillProperties(Map map, Properties properties) { + // notice that this is only + map.entrySet().forEach(entry -> { + if (entry.getValue() != null) + if (entry.getValue() instanceof Map) { + fillProperties((Map) entry.getValue(), properties); + } else { + if (entry.getValue() instanceof ArrayList) { + properties.put(entry.getKey(), String.join(Constants.WHITESPACE, (ArrayList) entry.getValue())); + } else { + properties.put(entry.getKey(), entry.getValue().toString()); + } + } + }); + } + + public static T getFromString(String json, Class typeParameterClass, boolean includeNulls) { + try { + if (includeNulls) { + T config = jsonMapperWithoutNulls.readValue(json, typeParameterClass); + return config; + } else { + T config = jsonMapper.readValue(json, typeParameterClass); + return config; + } + } catch (IOException e) { + logger.error(e.getMessage()); + logger.debug("{}", e.getStackTrace()); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/configuration/ConfigurationValidation.java b/src/main/java/org/whitesource/fs/configuration/ConfigurationValidation.java new file mode 100644 index 0000000..cc07244 --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/ConfigurationValidation.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import org.apache.commons.lang.StringUtils; +import org.whitesource.agent.Constants; + +import java.util.ArrayList; +import java.util.List; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +/** + * Author: eugen.horovitz + */ +public class ConfigurationValidation { + + // Check configuration errors + public List getConfigurationErrors(boolean projectPerFolder, String configProjectToken, String configProjectName, String configApiToken, String configFilePath, + int archiveDepth, String[] includes, String[] projectPerFolderIncludes, String[] pythonIncludes, String scanComment) { + List errors = new ArrayList<>(); + String[] requirements = pythonIncludes[Constants.ZERO].split(Constants.WHITESPACE); + if (StringUtils.isBlank(configApiToken)) { + String error = "Could not retrieve " + ORG_TOKEN_PROPERTY_KEY + " property from " + configFilePath; + errors.add(error); + } + boolean noProjectToken = StringUtils.isBlank(configProjectToken); + boolean noProjectName = StringUtils.isBlank(configProjectName); + + if (noProjectToken && noProjectName && !projectPerFolder) { + String error = "Could not retrieve properties " + PROJECT_NAME_PROPERTY_KEY + " and " + PROJECT_TOKEN_PROPERTY_KEY + " from " + configFilePath; + errors.add(error); + } else if (!noProjectToken && !noProjectName) { + String error = "Please choose just one of either " + PROJECT_NAME_PROPERTY_KEY + " or " + PROJECT_TOKEN_PROPERTY_KEY + " (and not both)"; + errors.add(error); + } + if (archiveDepth < Constants.ZERO || archiveDepth > Constants.MAX_EXTRACTION_DEPTH) { + errors.add("Error: archiveExtractionDepth value should be greater than 0 and less than " + Constants.MAX_EXTRACTION_DEPTH); + } + if (includes.length < Constants.ONE || StringUtils.isBlank(includes[Constants.ZERO])) { + errors.add("Error: includes parameter must have at list one scanning pattern"); + } + if (projectPerFolder && projectPerFolderIncludes == null) { + errors.add("projectPerFolderIncludes parameter is empty, specify folders to include or mark as comment to scan all folders"); + } + + if (requirements.length > Constants.ZERO) { + for (String requirement : requirements) { + if (!requirement.endsWith(Constants.TXT_EXTENSION)) { + String error = "Invalid file name: " + requirement + Constants.WHITESPACE + "in property" + PYTHON_REQUIREMENTS_FILE_INCLUDES + "from " + configFilePath; + errors.add(error); + } + } + } + // get user comment & check max valid size + if (!StringUtils.isBlank(scanComment)) { + if (scanComment.length() > Constants.COMMENT_MAX_LENGTH) { + errors.add("Error: scanComment parameters is longer than 1000 characters"); + } + } + return errors; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/configuration/EndPointConfiguration.java b/src/main/java/org/whitesource/fs/configuration/EndPointConfiguration.java new file mode 100644 index 0000000..6b9fa56 --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/EndPointConfiguration.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +public class EndPointConfiguration { + private final int port; + private final String certificate; + private final String pass; + private final boolean enabled; + private final boolean ssl; + + @JsonProperty(ENDPOINT_PORT) + public int getPort() { + return port; + } + + @JsonProperty(ENDPOINT_CERTIFICATE) + public String getCertificate() { + return certificate; + } + + @JsonProperty(ENDPOINT_PASS) + public String getPass() { + return pass; + } + + @JsonProperty(ENDPOINT_ENABLED) + public boolean isEnabled() { + return enabled; + } + + @JsonProperty(ENDPOINT_SSL_ENABLED) + public boolean isSsl() { + return ssl; + } + + @JsonCreator + public EndPointConfiguration( + @JsonProperty(ENDPOINT_PORT) int port, + @JsonProperty(ENDPOINT_CERTIFICATE) String certificate, + @JsonProperty(ENDPOINT_PASS) String pass, + @JsonProperty(ENDPOINT_ENABLED) boolean enabled, + @JsonProperty(ENDPOINT_SSL_ENABLED) boolean ssl) { + this.port = port; + this.certificate = certificate; + this.pass = pass; + this.enabled = enabled; + this.ssl = ssl; + } +} diff --git a/src/main/java/org/whitesource/fs/configuration/OfflineConfiguration.java b/src/main/java/org/whitesource/fs/configuration/OfflineConfiguration.java new file mode 100644 index 0000000..4a6216c --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/OfflineConfiguration.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.whitesource.agent.utils.WsStringUtils; +import org.whitesource.fs.FSAConfigProperty; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +public class OfflineConfiguration { + + @FSAConfigProperty + private final boolean offline; + @FSAConfigProperty + private final boolean zip; + @FSAConfigProperty + private final boolean prettyJson; + private final String whiteSourceFolderPath; + + @JsonCreator + public OfflineConfiguration( + @JsonProperty(OFFLINE_PROPERTY_KEY) boolean offline, + @JsonProperty(OFFLINE_ZIP_PROPERTY_KEY) boolean zip, + @JsonProperty(OFFLINE_PRETTY_JSON_KEY) boolean prettyJson, + @JsonProperty(WHITESOURCE_FOLDER_PATH) String whiteSourceFolderPath) { + this.offline = offline; + this.zip = zip; + this.prettyJson = prettyJson; + this.whiteSourceFolderPath = whiteSourceFolderPath; + } + + @JsonProperty(OFFLINE_PROPERTY_KEY) + public boolean isOffline() { + return offline; + } + + @JsonProperty(OFFLINE_PRETTY_JSON_KEY) + public boolean isPrettyJson() { + return prettyJson; + } + + @JsonProperty(OFFLINE_ZIP_PROPERTY_KEY) + public boolean isZip() { + return zip; + } + + @JsonProperty(WHITESOURCE_FOLDER_PATH) + public String getWhiteSourceFolderPath() { + return whiteSourceFolderPath; + } + + @Override + public String toString() { + return WsStringUtils.toString(this); + } +} diff --git a/src/main/java/org/whitesource/fs/configuration/RemoteDockerAWSConfiguration.java b/src/main/java/org/whitesource/fs/configuration/RemoteDockerAWSConfiguration.java new file mode 100644 index 0000000..e4013cf --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/RemoteDockerAWSConfiguration.java @@ -0,0 +1,42 @@ +/* +package org.whitesource.fs.configuration; + +import java.util.List; + +public class RemoteDockerAWSConfiguration extends RemoteDockerConfiguration { + + private String registryId; + private String region = "east"; + + public RemoteDockerAWSConfiguration() { + super(); + } + + public RemoteDockerAWSConfiguration(String registryId, String region, List repositoryNames, List imageTags, List imageDigests) { + super(repositoryNames, imageTags, imageDigests); + this.registryId = registryId; + this.region = region; + } + + public String getRegistryId() { + return registryId; + } + + public void setRegistryId(String registryId) { + this.registryId = registryId; + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public List getRepositoryNames() { + return super.getImageNames(); + } + +} +*/ \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/configuration/RemoteDockerConfiguration.java b/src/main/java/org/whitesource/fs/configuration/RemoteDockerConfiguration.java new file mode 100644 index 0000000..d550764 --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/RemoteDockerConfiguration.java @@ -0,0 +1,151 @@ +package org.whitesource.fs.configuration; + +import java.util.List; + +public class RemoteDockerConfiguration { + + // Global Docker configurations + private List imageNames; + private List imageTags; + private List imageDigests; + private boolean forceDelete; + private boolean remoteDockerEnabled; + private int maxScanImages; + private boolean forcePull; + private int maxPullImages; + private boolean loginSudo; + + // Amazon ECR configurations + private List amazonRegistryIds; + private boolean remoteDockerAmazonEnabled; + private String amazonRegion = "east"; + private int amazonMaxPullImages; + + // Azure configurations + private boolean remoteDockerAzureEnabled; + private String azureUserName; + private String azureUserPassword; + private List azureRegistryNames; + + /* --- Constructors --- */ + + public RemoteDockerConfiguration(List imageNames, List imageTags, List imageDigests, + boolean forceDelete, boolean remoteDockerEnabled, int maxScanImages, + boolean forcePull, int maxPullImages, boolean loginSudo) { + this.imageNames = imageNames; + this.imageTags = imageTags; + this.imageDigests = imageDigests; + this.forceDelete = forceDelete; + this.remoteDockerEnabled = remoteDockerEnabled; + this.maxScanImages = maxScanImages; + this.amazonMaxPullImages = 0; + this.forcePull = forcePull; + this.maxPullImages = maxPullImages; + this.loginSudo = loginSudo; + } + + /* --- Getters / Setters --- */ + + public List getImageNames() { + return imageNames; + } + + public List getImageTags() { + return imageTags; + } + + public List getImageDigests() { + return imageDigests; + } + + public boolean isForceDelete() { + return forceDelete; + } + + public void setForceDelete(boolean forceDelete) { + this.forceDelete = forceDelete; + } + + public boolean isRemoteDockerEnabled() { + return remoteDockerEnabled; + } + + public int getMaxScanImages() { + return maxScanImages; + } + + public boolean isForcePull() { + return forcePull; + } + + public int getMaxPullImages() { + return maxPullImages; + } + + public boolean isLoginSudo() { + return loginSudo; + } + + // ------------- Amazon methods ------------- + + public List getAmazonRegistryIds() { + return amazonRegistryIds; + } + + public void setAmazonRegistryIds(List amazonRegistryIds) { + this.amazonRegistryIds = amazonRegistryIds; + } + + public void setAmazonRegion(String amazonRegion) { + this.amazonRegion = amazonRegion; + } + + public boolean isRemoteDockerAmazonEnabled() { + return remoteDockerAmazonEnabled; + } + + public void setRemoteDockerAmazonEnabled(boolean remoteDockerAmazonEnabled) { + this.remoteDockerAmazonEnabled = remoteDockerAmazonEnabled; + } + + public int getAmazonMaxPullImages() { + return amazonMaxPullImages; + } + + public void setAmazonMaxPullImages(int amazonMaxPullImages) { + this.amazonMaxPullImages = amazonMaxPullImages; + } + + // ---------- Azure Methods -------------- + public boolean isRemoteDockerAzureEnabled() { + return remoteDockerAzureEnabled; + } + + public void setRemoteDockerAzureEnabled(boolean remoteDockerAzureEnabled) { + this.remoteDockerAzureEnabled = remoteDockerAzureEnabled; + } + + public String getAzureUserName() { + return azureUserName; + } + + public void setAzureUserName(String azureUserName) { + this.azureUserName = azureUserName; + } + + public String getAzureUserPassword() { + return azureUserPassword; + } + + public void setAzureUserPassword(String azureUserPassword) { + this.azureUserPassword = azureUserPassword; + } + + public List getAzureRegistryNames() { + return azureRegistryNames; + } + + public void setAzureRegistryNames(List azureRegistryNames) { + this.azureRegistryNames = azureRegistryNames; + } +} diff --git a/src/main/java/org/whitesource/fs/configuration/RequestConfiguration.java b/src/main/java/org/whitesource/fs/configuration/RequestConfiguration.java new file mode 100644 index 0000000..11e7ddb --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/RequestConfiguration.java @@ -0,0 +1,184 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang.StringUtils; +import org.whitesource.agent.utils.WsStringUtils; +import org.whitesource.fs.FSAConfigProperty; +import org.whitesource.fs.WsSecret; + +import java.util.List; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class RequestConfiguration { + + @WsSecret + @FSAConfigProperty + private final String apiToken; + @WsSecret + @FSAConfigProperty + private final String userKey; + @FSAConfigProperty + private final String projectVersion; + @FSAConfigProperty + private final String projectToken; + @FSAConfigProperty + private final boolean projectPerSubFolder; + @FSAConfigProperty + private final String requesterEmail; + @FSAConfigProperty + private final String productToken; + @FSAConfigProperty + private String productName; + @FSAConfigProperty + private String productVersion; + @FSAConfigProperty + private final String projectName; + private final List appPaths; + private final String viaDebug; + private final int viaAnalysisLevel; + private final String iaLanguage; + @FSAConfigProperty + private final String scanComment; + @FSAConfigProperty + private final boolean requireKnownSha1; + + public RequestConfiguration(@JsonProperty(ORG_TOKEN_PROPERTY_KEY) String apiToken, + @JsonProperty(USER_KEY_PROPERTY_KEY) String userKey, + @JsonProperty(REQUESTER_EMAIL) String requesterEmail, + @JsonProperty(PROJECT_PER_SUBFOLDER) boolean projectPerSubFolder, + @JsonProperty(PROJECT_NAME_PROPERTY_KEY) String projectName, + @JsonProperty(PROJECT_TOKEN_PROPERTY_KEY) String projectToken, + @JsonProperty(PROJECT_VERSION_PROPERTY_KEY) String projectVersion, + @JsonProperty(PRODUCT_NAME_PROPERTY_KEY) String productName, + @JsonProperty(PRODUCT_TOKEN_PROPERTY_KEY) String productToken, + @JsonProperty(PRODUCT_VERSION_PROPERTY_KEY) String productVersion, + @JsonProperty(APP_PATH) List appPaths, + @JsonProperty(VIA_DEBUG) String viaDebug, + @JsonProperty(VIA_ANALYSIS_LEVEL) int viaAnalysisLevel, + @JsonProperty(IA_LANGUAGE) String iaLanguage, + @JsonProperty(SCAN_COMMENT) String scanComment, + @JsonProperty(REQUIRE_KNOWN_SHA1) boolean requireKnownSha1) { + this.apiToken = apiToken; + this.userKey = userKey; + this.requesterEmail = requesterEmail; + this.projectPerSubFolder = projectPerSubFolder; + this.projectName = projectName; + this.projectToken = projectToken; + this.projectVersion = projectVersion; + this.productName = productName; + this.productToken = productToken; + this.productVersion = productVersion; + this.viaAnalysisLevel = viaAnalysisLevel; + this.appPaths = appPaths; + this.viaDebug = viaDebug; + this.iaLanguage = iaLanguage; + this.scanComment = scanComment; + this.requireKnownSha1 = requireKnownSha1; + } + + @JsonProperty(PROJECT_NAME_PROPERTY_KEY) + public String getProjectName() { + return projectName; + } + + @JsonProperty(PROJECT_VERSION_PROPERTY_KEY) + public String getProjectVersion() { + return projectVersion; + } + + @JsonProperty(PROJECT_TOKEN_PROPERTY_KEY) + public String getProjectToken() { + return projectToken; + } + + @JsonProperty(PRODUCT_TOKEN_PROPERTY_KEY) + public String getProductToken() { + return productToken; + } + + @JsonProperty(PRODUCT_NAME_PROPERTY_KEY) + public String getProductName() { + return productName; + } + + @JsonProperty(PRODUCT_VERSION_PROPERTY_KEY) + public String getProductVersion() { + return productVersion; + } + + @JsonProperty(PROJECT_PER_SUBFOLDER) + public boolean isProjectPerSubFolder() { + return projectPerSubFolder; + } + + @JsonProperty(REQUESTER_EMAIL) + public String getRequesterEmail() { + return requesterEmail; + } + + @JsonProperty(ORG_TOKEN_PROPERTY_KEY) + public String getApiToken() { + return apiToken; + } + + @JsonProperty(APP_PATH) + public List getAppPaths() { + return appPaths; + } + + @JsonProperty(VIA_DEBUG) + public String getViaDebug() { + return viaDebug; + } + + @JsonProperty(VIA_ANALYSIS_LEVEL) + public int getViaAnalysisLevel() { + return viaAnalysisLevel; + } + + @JsonProperty(USER_KEY_PROPERTY_KEY) + public String getUserKey() { return userKey; } + + @JsonProperty(IA_LANGUAGE) + public String getIaLanguage() { + return iaLanguage; + } + + @JsonProperty(SCAN_COMMENT) + public String getScanComment() { + return scanComment; + } + + @JsonProperty(REQUIRE_KNOWN_SHA1) + public boolean isRequireKnownSha1() { return requireKnownSha1; } + + public String getProductNameOrToken() { + if (StringUtils.isBlank(getProductToken())) { + return getProductName(); + } + return getProductToken(); + } + + @Override + public String toString() { + return WsStringUtils.toString(this); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/configuration/ResolverConfiguration.java b/src/main/java/org/whitesource/fs/configuration/ResolverConfiguration.java new file mode 100644 index 0000000..f4237e3 --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/ResolverConfiguration.java @@ -0,0 +1,884 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.dependency.resolver.go.GoDependencyManager; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.fs.FSAConfigProperty; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +public class ResolverConfiguration { + + + /* --- Constructors --- */ + + @JsonCreator + public ResolverConfiguration( + @JsonProperty(NPM_RUN_PRE_STEP) boolean npmRunPreStep, + @JsonProperty(NPM_RESOLVE_DEPENDENCIES) boolean npmResolveDependencies, + @JsonProperty(NPM_IGNORE_SCRIPTS) boolean npmIgnoreScripts, + @JsonProperty(NPM_INCLUDE_DEV_DEPENDENCIES) boolean npmIncludeDevDependencies, + @JsonProperty(NPM_IGNORE_SOURCE_FILES) boolean npmIgnoreSourceFiles, + @JsonProperty(NPM_TIMEOUT_DEPENDENCIES_COLLECTOR_SECONDS) long npmTimeoutDependenciesCollector, + @JsonProperty(NPM_ACCESS_TOKEN) String npmAccessToken, + @JsonProperty(NPM_IGNORE_NPM_LS_ERRORS) boolean npmIgnoreNpmLsErrors, + @JsonProperty(NPM_YARN_PROJECT) boolean npmYarnProject, + + @JsonProperty(BOWER_RESOLVE_DEPENDENCIES) boolean bowerResolveDependencies, + @JsonProperty(BOWER_RUN_PRE_STEP) boolean bowerRunPreStep, + @JsonProperty(BOWER_IGNORE_SOURCE_FILES) boolean bowerIgnoreSourceFiles, + + @JsonProperty(NUGET_RESOLVE_DEPENDENCIES) boolean nugetResolveDependencies, + @JsonProperty(NUGET_RESTORE_DEPENDENCIES) boolean nugetRestoreDependencies, + @JsonProperty(NUGET_RUN_PRE_STEP) boolean nugetRunPreStep, + @JsonProperty(NUGET_IGNORE_SOURCE_FILES) boolean nugetIgnoreSourceFiles, + @JsonProperty(NUGET_RESOLVE_PACKAGES_CONFIG_FILES) boolean nugetResolvePackagesConfigFiles, + @JsonProperty(NUGET_RESOLVE_CS_PROJ_FILES) boolean nugetResolveCsProjFiles, + + @JsonProperty(MAVEN_RESOLVE_DEPENDENCIES) boolean mavenResolveDependencies, + @JsonProperty(MAVEN_IGNORED_SCOPES) String[] mavenIgnoredScopes, + @JsonProperty(MAVEN_AGGREGATE_MODULES) boolean mavenAggregateModules, + @JsonProperty(MAVEN_IGNORE_POM_MODULES) boolean mavenIgnorePomModules, + @JsonProperty(MAVEN_IGNORE_SOURCE_FILES) boolean mavenIgnoreSourceFiles, + @JsonProperty(MAVEN_RUN_PRE_STEP) boolean mavenRunPreStep, + @JsonProperty(MAVEN_IGNORE_DEPENDENCY_TREE_ERRORS) boolean mavenIgnoreDependencyTreeErrors, + + @JsonProperty(PYTHON_RESOLVE_DEPENDENCIES) boolean pythonResolveDependencies, + @JsonProperty(PYTHON_PIP_PATH) String pipPath, + @JsonProperty(PYTHON_PATH) String pythonPath, + @JsonProperty(PYTHON_IS_WSS_PLUGIN_INSTALLED) boolean pythonIsWssPluginInstalled, + @JsonProperty(PYTHON_UNINSTALL_WSS_PLUGIN) boolean pythonUninstallWssPlugin, + @JsonProperty(PYTHON_IGNORE_PIP_INSTALL_ERRORS) boolean pythonIgnorePipInstallErrors, + @JsonProperty(PYTHON_INSTALL_VIRTUALENV) boolean pythonInstallVirtualenv, + @JsonProperty(PYTHON_RESOLVE_HIERARCHY_TREE) boolean pythonResolveHierarchyTree, + @JsonProperty(PYTHON_REQUIREMENTS_FILE_INCLUDES) String[] pythonRequirementsFileIncludes, + @JsonProperty(PYTHON_RESOLVE_SETUP_PY_FILES) boolean pythonResolveSetupPyFiles, + @JsonProperty(PYTHON_IGNORE_SOURCE_FILES) boolean pythonIgnoreSourceFiles, + @JsonProperty(PYTHON_IGNORE_PIPENV_INSTALL_ERRORS) boolean ignorePipEnvInstallErrors, + @JsonProperty(PYTHON_RUN_PIPENV_PRE_STEP) boolean runPipenvPreStep, + @JsonProperty(PYTHON_PIPENV_DEV_DEPENDENCIES) boolean pipenvInstallDevDependencies, + @JsonProperty(IGNORE_SOURCE_FILES) boolean ignoreSourceFiles, + // @JsonProperty(DEPENDENCIES_ONLY) boolean dependenciesOnly, + @JsonProperty(WHITESOURCE_CONFIGURATION) String whitesourceConfiguration, + + @JsonProperty(GRADLE_RESOLVE_DEPENDENCIES) boolean gradleResolveDependencies, + @JsonProperty(GRADLE_RUN_ASSEMBLE_COMMAND) boolean gradleRunAssembleCommand, + @JsonProperty(GRADLE_AGGREGATE_MODULES) boolean gradleAggregateModules, + @JsonProperty(GRADLE_PREFERRED_ENVIRONMENT) String gradlePreferredEnvironment, + @JsonProperty(GRADLE_IGNORE_SOURCE_FILES) boolean gradleIgnoreSourceFiles, + @JsonProperty(GRADLE_RUN_PRE_STEP) boolean gradleRunPreStep, + @JsonProperty(GRADLE_IGNORE_SCOPES) String[] gradleIgnoredScopes, + @JsonProperty(GRADLE_LOCAL_REPOSITORY_PATH) String gradleLocalRepositoryPath, + + @JsonProperty(PAKET_RESOLVE_DEPENDENCIES) boolean paketResolveDependencies, + @JsonProperty(PAKET_IGNORED_GROUPS) String[] paketIgnoredScopes, + @JsonProperty(PAKET_RUN_PRE_STEP) boolean paketRunPreStep, + @JsonProperty(PAKET_EXE_PATH) String paketPath, + @JsonProperty(PAKET_IGNORE_SOURCE_FILES) boolean paketIgnoreSourceFiles, + + @JsonProperty(GO_RESOLVE_DEPENDENCIES) boolean goResolveDependencies, + @JsonProperty(GO_DEPENDENCY_MANAGER) String goDependencyManager, + @JsonProperty(GO_COLLECT_DEPENDENCIES_AT_RUNTIME) boolean goCollectDependenciesAtRuntime, + @JsonProperty(GO_GLIDE_IGNORE_TEST_PACKAGES) boolean goIgnoreTestPackages, + @JsonProperty(GO_IGNORE_SOURCE_FILES) boolean goIgnoreSourceFiles, + @JsonProperty(GO_GRADLE_ENABLE_TASK_ALIAS) boolean goGradleEnableTaskAlias, + + @JsonProperty(RUBY_RESOLVE_DEPENDENCIES) boolean rubyResolveDependencies, + @JsonProperty(RUBY_RUN_BUNDLE_INSTALL) boolean rubyRunBundleInstall, + @JsonProperty(RUBY_OVERWRITE_GEM_FILE) boolean rubyOverwriteGemFile, + @JsonProperty(RUBY_INSTALL_MISSING_GEMS) boolean rubyInstallMissingGems, + @JsonProperty(RUBY_IGNORE_SOURCE_FILES) boolean rubyIgnoreSourceFiles, + + @JsonProperty(PHP_RESOLVE_DEPENDENCIES) boolean phpResolveDependencies, + @JsonProperty(PHP_RUN_PRE_STEP) boolean phpRunPreStep, + @JsonProperty(PHP_INCLUDE_DEV_DEPENDENCIES) boolean phpIncludeDevDependencies, + + @JsonProperty(SBT_RESOLVE_DEPENDENCIES) boolean sbtResolveDependencies, + @JsonProperty(SBT_AGGREGATE_MODULES) boolean sbtAggregateModules, + @JsonProperty(SBT_RUN_PRE_STEP) boolean sbtRunPreStep, + @JsonProperty(SBT_TARGET_FOLDER) String sbtTargetFolder, + @JsonProperty(SBT_IGNORE_SOURCE_FILES) boolean sbtIgnoreSourceFiles, + + @JsonProperty(HTML_RESOLVE_DEPENDENCIES) boolean htmlResolveDependencies, + @JsonProperty(COCOAPODS_RESOLVE_DEPENDENCIES) boolean cocoapodsResolveDependencies, + @JsonProperty(COCOAPODS_RUN_PRE_STEP) boolean cocoapodsRunPreStep, + @JsonProperty(COCOAPODS_IGNORE_SOURCE_FILES) boolean cocoapodsIgnoreSourceFiles, + @JsonProperty(HEX_RESOLVE_DEPENDENECIES) boolean hexResolveDependencies, + @JsonProperty(HEX_RUN_PRE_STEP) boolean hexRunPreStep, + @JsonProperty(HEX_IGNORE_SOURCE_FILES) boolean hexIgnoreSourceFiles, + @JsonProperty(HEX_AGGREGATE_MODULES) boolean hexAggregateModules, + @JsonProperty("addSha1") boolean addSha1) { + this.npmRunPreStep = npmRunPreStep; + this.npmIgnoreScripts = npmIgnoreScripts; + this.npmResolveDependencies = npmResolveDependencies; + this.npmIncludeDevDependencies = npmIncludeDevDependencies; + this.npmTimeoutDependenciesCollector = npmTimeoutDependenciesCollector; + this.npmAccessToken = npmAccessToken; + this.npmIgnoreNpmLsErrors = npmIgnoreNpmLsErrors; + this.npmYarnProject = npmYarnProject; + this.npmIgnoreSourceFiles = npmIgnoreSourceFiles; + + this.bowerResolveDependencies = bowerResolveDependencies; + this.bowerRunPreStep = bowerRunPreStep; + this.bowerIgnoreSourceFiles = bowerIgnoreSourceFiles; + + this.nugetResolveDependencies = nugetResolveDependencies; + this.nugetRestoreDependencies = nugetRestoreDependencies; + this.nugetRunPreStep = nugetRunPreStep; + this.nugetIgnoreSourceFiles = nugetIgnoreSourceFiles; + this.nugetResolveCsProjFiles = nugetResolveCsProjFiles; + this.nugetResolvePackagesConfigFiles = nugetResolvePackagesConfigFiles; + + this.mavenResolveDependencies = mavenResolveDependencies; + this.mavenIgnoredScopes = mavenIgnoredScopes; + this.mavenAggregateModules = mavenAggregateModules; + this.mavenIgnorePomModules = mavenIgnorePomModules; + this.mavenIgnoreSourceFiles = mavenIgnoreSourceFiles; + this.mavenRunPreStep = mavenRunPreStep; + this.mavenIgnoreDependencyTreeErrors = mavenIgnoreDependencyTreeErrors; + + this.pythonResolveDependencies = pythonResolveDependencies; + this.pipPath = pipPath; + this.pythonPath = pythonPath; + this.pythonIsWssPluginInstalled = pythonIsWssPluginInstalled; + this.pythonUninstallWssPlugin = pythonUninstallWssPlugin; + this.pythonIgnorePipInstallErrors = pythonIgnorePipInstallErrors; + this.pythonInstallVirtualenv = pythonInstallVirtualenv; + this.pythonResolveHierarchyTree = pythonResolveHierarchyTree; + this.pythonRequirementsFileIncludes = pythonRequirementsFileIncludes; + this.pythonResolveSetupPyFiles = pythonResolveSetupPyFiles; + this.pythonIgnoreSourceFiles = pythonIgnoreSourceFiles; + this.ignorePipEnvInstallErrors = ignorePipEnvInstallErrors; + this.runPipenvPreStep = runPipenvPreStep; + this.pipenvInstallDevDependencies = pipenvInstallDevDependencies; + this.ignoreSourceFiles = ignoreSourceFiles; + this.whitesourceConfiguration = whitesourceConfiguration; + + this.gradleResolveDependencies = gradleResolveDependencies; + this.gradleAggregateModules = gradleAggregateModules; + this.gradleRunAssembleCommand = gradleRunAssembleCommand; + this.gradlePreferredEnvironment = gradlePreferredEnvironment; + this.gradleIgnoreSourceFiles = gradleIgnoreSourceFiles; + this.gradleRunPreStep = gradleRunPreStep; + this.gradleIgnoredScopes = gradleIgnoredScopes; + this.gradleLocalRepositoryPath = gradleLocalRepositoryPath; + + this.paketResolveDependencies = paketResolveDependencies; + this.paketIgnoredScopes = paketIgnoredScopes; + this.paketRunPreStep = paketRunPreStep; + this.paketPath = paketPath; + this.paketIgnoreSourceFiles = paketIgnoreSourceFiles; + + this.goResolveDependencies = goResolveDependencies; + if (goDependencyManager != null && !goDependencyManager.isEmpty()) { + this.goDependencyManager = GoDependencyManager.getFromType(goDependencyManager); + } + this.goCollectDependenciesAtRuntime = goCollectDependenciesAtRuntime; + this.goGlideIgnoreTestPackages = goIgnoreTestPackages; + this.goGlideIgnoreSourceFiles = goIgnoreSourceFiles; + this.goGradleEnableTaskAlias = goGradleEnableTaskAlias; + + this.rubyResolveDependencies = rubyResolveDependencies; + this.rubyRunBundleInstall = rubyRunBundleInstall; + this.rubyOverwriteGemFile = rubyOverwriteGemFile; + this.rubyInstallMissingGems = rubyInstallMissingGems; + this.rubyIgnoreSourceFiles = rubyIgnoreSourceFiles; + + this.phpResolveDependencies = phpResolveDependencies; + this.phpRunPreStep = phpRunPreStep; + this.phpIncludeDevDependencies = phpIncludeDevDependencies; + + this.sbtResolveDependencies = sbtResolveDependencies; + this.sbtAggregateModules = sbtAggregateModules; + this.sbtRunPreStep = sbtRunPreStep; + this.sbtTargetFolder = sbtTargetFolder; + this.sbtIgnoreSourceFiles = sbtIgnoreSourceFiles; + + this.htmlResolveDependencies = htmlResolveDependencies; + + this.cocoapodsResolveDependencies = cocoapodsResolveDependencies; + this.cocoapodsRunPreStep = cocoapodsRunPreStep; + this.cocoapodsIgnoreSourceFiles = cocoapodsIgnoreSourceFiles; + + this.hexResolveDependencies = hexResolveDependencies; + this.hexRunPreStep = hexRunPreStep; + this.hexIgnoreSourceFiles = hexIgnoreSourceFiles; + this.hexAggregateModules = hexAggregateModules; + + this.addSha1 = addSha1; + } + + /* --- Members --- */ + + private static Logger logger; + private String whitesourceConfiguration; + + @FSAConfigProperty + private boolean ignoreSourceFiles; + + @FSAConfigProperty + private boolean npmRunPreStep; + @FSAConfigProperty + private boolean npmIgnoreScripts; + @FSAConfigProperty + private boolean npmResolveDependencies; + @FSAConfigProperty + private boolean npmIncludeDevDependencies; + @FSAConfigProperty + private long npmTimeoutDependenciesCollector; + @FSAConfigProperty + private boolean npmIgnoreNpmLsErrors; + @FSAConfigProperty + private boolean npmYarnProject; + @FSAConfigProperty + private boolean npmIgnoreSourceFiles; + private String npmAccessToken; + + @FSAConfigProperty + private boolean bowerResolveDependencies; + @FSAConfigProperty + private boolean bowerRunPreStep; + @FSAConfigProperty + private boolean bowerIgnoreSourceFiles; + + @FSAConfigProperty + private boolean nugetResolveDependencies; + @FSAConfigProperty + private boolean nugetRestoreDependencies; + @FSAConfigProperty + private boolean nugetRunPreStep; + @FSAConfigProperty + private boolean nugetIgnoreSourceFiles; + @FSAConfigProperty + private boolean nugetResolvePackagesConfigFiles; + @FSAConfigProperty + private boolean nugetResolveCsProjFiles; + + @FSAConfigProperty + private boolean mavenResolveDependencies; + @FSAConfigProperty + private String[] mavenIgnoredScopes; + @FSAConfigProperty + private boolean mavenAggregateModules; + @FSAConfigProperty + private boolean mavenIgnorePomModules; + @FSAConfigProperty + private boolean mavenIgnoreSourceFiles; + @FSAConfigProperty + private boolean mavenRunPreStep; + @FSAConfigProperty + private boolean mavenIgnoreDependencyTreeErrors; + + @FSAConfigProperty + private boolean pythonResolveDependencies; + @FSAConfigProperty + private String pipPath; + @FSAConfigProperty + private String pythonPath; + @FSAConfigProperty + private boolean pythonIgnorePipInstallErrors; + @FSAConfigProperty + private boolean pythonInstallVirtualenv; + @FSAConfigProperty + private boolean pythonResolveHierarchyTree; + @FSAConfigProperty + private String[] pythonRequirementsFileIncludes; + @FSAConfigProperty + private boolean pythonResolveSetupPyFiles; + @FSAConfigProperty + private boolean pythonIgnoreSourceFiles; + @FSAConfigProperty + private boolean ignorePipEnvInstallErrors; + @FSAConfigProperty + private boolean pipenvInstallDevDependencies; + @FSAConfigProperty + private boolean runPipenvPreStep; + @FSAConfigProperty + private final boolean pythonIsWssPluginInstalled; + @FSAConfigProperty + private final boolean pythonUninstallWssPlugin; + + @FSAConfigProperty + private boolean gradleResolveDependencies; + @FSAConfigProperty + private boolean gradleRunAssembleCommand; + @FSAConfigProperty + private boolean gradleAggregateModules; + @FSAConfigProperty + private String gradlePreferredEnvironment; + @FSAConfigProperty + private String gradleLocalRepositoryPath; + @FSAConfigProperty + private boolean gradleIgnoreSourceFiles; + @FSAConfigProperty + private boolean gradleRunPreStep; + @FSAConfigProperty + private String[] gradleIgnoredScopes; + + @FSAConfigProperty + private boolean paketResolveDependencies; + @FSAConfigProperty + private String[] paketIgnoredScopes; + @FSAConfigProperty + private boolean paketRunPreStep; + @FSAConfigProperty + private String paketPath; + @FSAConfigProperty + private boolean paketIgnoreSourceFiles; + + @FSAConfigProperty + private boolean goResolveDependencies; + @FSAConfigProperty + private GoDependencyManager goDependencyManager; + @FSAConfigProperty + private boolean goCollectDependenciesAtRuntime; + @FSAConfigProperty + private boolean goGlideIgnoreTestPackages; + @FSAConfigProperty + private boolean goGlideIgnoreSourceFiles; + @FSAConfigProperty + private boolean goGradleEnableTaskAlias; + + @FSAConfigProperty + private boolean rubyResolveDependencies; + @FSAConfigProperty + private boolean rubyRunBundleInstall; + @FSAConfigProperty + private boolean rubyOverwriteGemFile; + @FSAConfigProperty + private boolean rubyInstallMissingGems; + @FSAConfigProperty + private boolean rubyIgnoreSourceFiles; + + @FSAConfigProperty + private boolean phpResolveDependencies; + @FSAConfigProperty + private boolean phpRunPreStep; + @FSAConfigProperty + private boolean phpIncludeDevDependencies; + + @FSAConfigProperty + private boolean sbtResolveDependencies; + @FSAConfigProperty + private boolean sbtAggregateModules; + @FSAConfigProperty + private boolean sbtRunPreStep; + @FSAConfigProperty + private String sbtTargetFolder; + @FSAConfigProperty + private boolean sbtIgnoreSourceFiles; + + @FSAConfigProperty + private boolean htmlResolveDependencies; + + @FSAConfigProperty + private boolean cocoapodsResolveDependencies; + @FSAConfigProperty + private boolean cocoapodsRunPreStep; + @FSAConfigProperty + private boolean cocoapodsIgnoreSourceFiles; + + @FSAConfigProperty + private boolean hexResolveDependencies; + @FSAConfigProperty + private boolean hexRunPreStep; + @FSAConfigProperty + private boolean hexAggregateModules; + @FSAConfigProperty + private boolean hexIgnoreSourceFiles; + + private boolean addSha1; + + /* --- Public getters --- */ + + @JsonProperty(NPM_RUN_PRE_STEP) + public boolean isNpmRunPreStep() { + return npmRunPreStep; + } + + @JsonProperty(NPM_IGNORE_SCRIPTS) + public boolean isNpmIgnoreScripts() { + return npmIgnoreScripts; + } + + @JsonProperty(NPM_RESOLVE_DEPENDENCIES) + public boolean isNpmResolveDependencies() { + return npmResolveDependencies; + } + + @JsonProperty(NPM_INCLUDE_DEV_DEPENDENCIES) + public boolean isNpmIncludeDevDependencies() { + return npmIncludeDevDependencies; + } + + @JsonProperty(NPM_IGNORE_SOURCE_FILES) + public boolean isNpmIgnoreSourceFiles() { + return npmIgnoreSourceFiles; + } + + @JsonProperty(NPM_TIMEOUT_DEPENDENCIES_COLLECTOR_SECONDS) + public long getNpmTimeoutDependenciesCollector() { + return npmTimeoutDependenciesCollector; + } + + @JsonProperty(NPM_ACCESS_TOKEN) + public String getNpmAccessToken() { + return npmAccessToken; + } + + @JsonProperty(NPM_IGNORE_NPM_LS_ERRORS) + public boolean getNpmIgnoreNpmLsErrors() { + return npmIgnoreNpmLsErrors; + } + + @JsonProperty(NPM_YARN_PROJECT) + public boolean getNpmYarnProject() { + return npmYarnProject; + } + + @JsonProperty(BOWER_RESOLVE_DEPENDENCIES) + public boolean isBowerResolveDependencies() { + return bowerResolveDependencies; + } + + @JsonProperty(BOWER_RUN_PRE_STEP) + public boolean isBowerRunPreStep() { + return bowerRunPreStep; + } + + @JsonProperty(BOWER_IGNORE_SOURCE_FILES) + public boolean isBowerIgnoreSourceFiles() { + return bowerIgnoreSourceFiles; + } + + @JsonProperty(NUGET_RESOLVE_DEPENDENCIES) + public boolean isNugetResolveDependencies() { + return nugetResolveDependencies; + } + + @JsonProperty(NUGET_RESTORE_DEPENDENCIES) + public boolean isNugetRestoreDependencies() { + return nugetRestoreDependencies; + } + + @JsonProperty(NUGET_RUN_PRE_STEP) + public boolean isNugetRunPreStep() { + return nugetRunPreStep; + } + + @JsonProperty(NUGET_RESOLVE_CS_PROJ_FILES) + public boolean isNugetResolveCsProjFiles() { + return nugetResolveCsProjFiles; + } + + @JsonProperty(NUGET_RESOLVE_PACKAGES_CONFIG_FILES) + public boolean isNugetResolvePackagesConfigFiles() { + return nugetResolvePackagesConfigFiles; + } + + @JsonProperty(NUGET_IGNORE_SOURCE_FILES) + public boolean isNugetIgnoreSourceFiles() { + return nugetIgnoreSourceFiles; + } + + @JsonProperty(MAVEN_RESOLVE_DEPENDENCIES) + public boolean isMavenResolveDependencies() { + return mavenResolveDependencies; + } + + @JsonProperty(MAVEN_IGNORED_SCOPES) + public String[] getMavenIgnoredScopes() { + return mavenIgnoredScopes; + } + + @JsonProperty(MAVEN_AGGREGATE_MODULES) + public boolean isMavenAggregateModules() { + return mavenAggregateModules; + } + + @JsonProperty(MAVEN_IGNORE_POM_MODULES) + public boolean isMavenIgnorePomModules() { + return mavenIgnorePomModules; + } + + @JsonProperty(MAVEN_IGNORE_SOURCE_FILES) + public boolean isMavenIgnoreSourceFiles() { + return mavenIgnoreSourceFiles; + } + + @JsonProperty(MAVEN_RUN_PRE_STEP) + public boolean isMavenRunPreStep() { + return mavenRunPreStep; + } + + @JsonProperty(MAVEN_IGNORE_DEPENDENCY_TREE_ERRORS) + public boolean isMavenIgnoreDependencyTreeErrors() { + return mavenIgnoreDependencyTreeErrors; + } + + @JsonProperty(IGNORE_SOURCE_FILES) + public boolean isIgnoreSourceFiles() { + return ignoreSourceFiles; + } + + @JsonProperty(WHITESOURCE_CONFIGURATION) + public String getWhitesourceConfiguration() { + return whitesourceConfiguration; + } + + @JsonProperty(PYTHON_RESOLVE_DEPENDENCIES) + public boolean isPythonResolveDependencies() { + return pythonResolveDependencies; + } + + @JsonProperty(PYTHON_PIP_PATH) + public String getPipPath() { + return pipPath; + } + + @JsonProperty(PYTHON_PATH) + public String getPythonPath() { + return pythonPath; + } + + @JsonProperty(PYTHON_IGNORE_PIP_INSTALL_ERRORS) + public boolean isPythonIgnorePipInstallErrors() { + return pythonIgnorePipInstallErrors; + } + + @JsonProperty(PYTHON_IS_WSS_PLUGIN_INSTALLED) + public boolean isPythonIsWssPluginInstalled() { + return pythonIsWssPluginInstalled; + } + + @JsonProperty(PYTHON_UNINSTALL_WSS_PLUGIN) + public boolean getPythonUninstallWssPlugin() { + return pythonUninstallWssPlugin; + } + + @JsonProperty(PYTHON_INSTALL_VIRTUALENV) + public boolean isPythonInstallVirtualenv() { + return pythonInstallVirtualenv; + } + + @JsonProperty(PYTHON_RESOLVE_HIERARCHY_TREE) + public boolean isPythonResolveHierarchyTree() { + return pythonResolveHierarchyTree; + } + + @JsonProperty(PYTHON_RESOLVE_SETUP_PY_FILES) + public boolean isPythonResolveSetupPyFiles() { + return this.pythonResolveSetupPyFiles; + } + + public String[] getPythonRequirementsFileIncludes() { + return pythonRequirementsFileIncludes; + } + + @JsonProperty(PYTHON_IGNORE_SOURCE_FILES) + public boolean isPythonIgnoreSourceFiles() { + return pythonIgnoreSourceFiles; + } + + @JsonProperty(PYTHON_RUN_PIPENV_PRE_STEP) + public boolean IsRunPipenvPreStep() { + return this.runPipenvPreStep; + } + + @JsonProperty(PYTHON_IGNORE_PIPENV_INSTALL_ERRORS) + public boolean isIgnorePipEnvInstallErrors() { + return this.ignorePipEnvInstallErrors; + } + + @JsonProperty(PYTHON_PIPENV_DEV_DEPENDENCIES) + public boolean isPipenvInstallDevDependencies() { + return pipenvInstallDevDependencies; + } + + @JsonProperty(GRADLE_RESOLVE_DEPENDENCIES) + public boolean isGradleResolveDependencies() { + return gradleResolveDependencies; + } + + @JsonProperty(GRADLE_AGGREGATE_MODULES) + public boolean isGradleAggregateModules() { + return gradleAggregateModules; + } + + @JsonProperty(GRADLE_RUN_ASSEMBLE_COMMAND) + public boolean isGradleRunAssembleCommand() { + return gradleRunAssembleCommand; + } + + @JsonProperty(GRADLE_PREFERRED_ENVIRONMENT) + public String getGradlePreferredEnvironment() { + return gradlePreferredEnvironment; + } + + @JsonProperty(GRADLE_IGNORE_SOURCE_FILES) + public boolean isGradleIgnoreSourceFiles() { + return gradleIgnoreSourceFiles; + } + + @JsonProperty(GRADLE_RUN_PRE_STEP) + public boolean isGradleRunPreStep() { + return gradleRunPreStep; + } + + @JsonProperty(GRADLE_LOCAL_REPOSITORY_PATH) + public String getGradleLocalRepositoryPath() { + return gradleLocalRepositoryPath; + } + + @JsonProperty(PAKET_RESOLVE_DEPENDENCIES) + public boolean isPaketResolveDependencies() { + return paketResolveDependencies; + } + + @JsonProperty(PAKET_IGNORED_GROUPS) + public String[] getPaketIgnoredScopes() { + return paketIgnoredScopes; + } + + @JsonProperty(PAKET_RUN_PRE_STEP) + public boolean isPaketRunPreStep() { + return paketRunPreStep; + } + + @JsonProperty(PAKET_EXE_PATH) + public String getPaketPath() { + return paketPath; + } + + @JsonProperty(PAKET_IGNORE_SOURCE_FILES) + public boolean isPaketIgnoreSourceFiles() { + return paketIgnoreSourceFiles; + } + + @JsonProperty(GO_RESOLVE_DEPENDENCIES) + public boolean isGoResolveDependencies() { + return goResolveDependencies; + } + + @JsonProperty(GO_DEPENDENCY_MANAGER) + public GoDependencyManager getGoDependencyManager() { + return goDependencyManager; + } + + @JsonProperty(GO_COLLECT_DEPENDENCIES_AT_RUNTIME) + public boolean isGoCollectDependenciesAtRuntime() { + return goCollectDependenciesAtRuntime; + } + + @JsonProperty(GO_GLIDE_IGNORE_TEST_PACKAGES) + public boolean isGoGlideIgnoreTestPackages() { + return goGlideIgnoreTestPackages; + } + + @JsonProperty(GO_IGNORE_SOURCE_FILES) + public boolean isGoGlideIgnoreSourceFiles() { + return goGlideIgnoreSourceFiles; + } + + @JsonProperty(GO_GRADLE_ENABLE_TASK_ALIAS) + public boolean isGoGradleEnableTaskAlias() { + return goGradleEnableTaskAlias; + } + + @JsonProperty(RUBY_RESOLVE_DEPENDENCIES) + public boolean isRubyResolveDependencies() { + return rubyResolveDependencies; + } + + @JsonProperty(RUBY_RUN_BUNDLE_INSTALL) + public boolean isRubyRunBundleInstall() { + return rubyRunBundleInstall; + } + + @JsonProperty(RUBY_OVERWRITE_GEM_FILE) + public boolean isRubyOverwriteGemFile() { + return rubyOverwriteGemFile; + } + + @JsonProperty(RUBY_INSTALL_MISSING_GEMS) + public boolean isRubyInstallMissingGems() { + return rubyInstallMissingGems; + } + + @JsonProperty(RUBY_IGNORE_SOURCE_FILES) + public boolean isRubyIgnoreSourceFiles() { + return rubyIgnoreSourceFiles; + } + + @JsonProperty(PHP_RESOLVE_DEPENDENCIES) + public boolean isPhpResolveDependencies() { + return phpResolveDependencies; + } + + @JsonProperty(PHP_RUN_PRE_STEP) + public boolean isPhpRunPreStep() { + return phpRunPreStep; + } + + @JsonProperty(PHP_INCLUDE_DEV_DEPENDENCIES) + public boolean isPhpIncludeDevDependencies() { + return phpIncludeDevDependencies; + } + + @JsonProperty(SBT_RESOLVE_DEPENDENCIES) + public boolean isSbtResolveDependencies() { + return sbtResolveDependencies; + } + + @JsonProperty(SBT_AGGREGATE_MODULES) + public boolean isSbtAggregateModules() { + return sbtAggregateModules; + } + + @JsonProperty(SBT_RUN_PRE_STEP) + public boolean isSbtRunPreStep() { + return sbtRunPreStep; + } + + @JsonProperty(SBT_TARGET_FOLDER) + public String getSbtTargetFolder() { + return sbtTargetFolder; + } + + @JsonProperty(SBT_IGNORE_SOURCE_FILES) + public boolean isSbtIgnoreSourceFiles() { + return sbtIgnoreSourceFiles; + } + + @JsonProperty(HTML_RESOLVE_DEPENDENCIES) + public boolean isHtmlResolveDependencies() { + return htmlResolveDependencies; + } + + @JsonProperty(COCOAPODS_RESOLVE_DEPENDENCIES) + public boolean isCocoapodsResolveDependencies() { + return cocoapodsResolveDependencies; + } + + @JsonProperty(COCOAPODS_RUN_PRE_STEP) + public boolean isCocoapodsRunPreStep() { + return cocoapodsRunPreStep; + } + + @JsonProperty(COCOAPODS_IGNORE_SOURCE_FILES) + public boolean isCocoapodsIgnoreSourceFiles() { + return cocoapodsIgnoreSourceFiles; + } + + @JsonProperty(HEX_RESOLVE_DEPENDENECIES) + public boolean isHexResolveDependencies() { + return hexResolveDependencies; + } + + @JsonProperty(HEX_IGNORE_SOURCE_FILES) + public boolean isHexIgnoreSourceFiles() { + return hexIgnoreSourceFiles; + } + + @JsonProperty(HEX_RUN_PRE_STEP) + public boolean isHexRunPreStep() { + return hexRunPreStep; + } + + @JsonProperty(HEX_AGGREGATE_MODULES) + public boolean isHexAggregateModules() { + return hexAggregateModules; + } + + public String[] getGradleIgnoredScopes() { + return this.gradleIgnoredScopes; + } + + public boolean isAddSha1() { + return addSha1; + } + + @Override + public String toString() { + logger = LoggerFactory.getLogger(ResolverConfiguration.class); + StringBuilder result = new StringBuilder(); + Field[] fields = this.getClass().getDeclaredFields(); + String newResolver; + String currentResolver = null; + List resolversList = Arrays.asList("npm", "bower", "nuget", "maven", "python", "gradle", "paket", "go", "ruby", "php", "sbt", "html", "cocoapods", "hex"); + for (Field field : fields) { + if (field.isAnnotationPresent(FSAConfigProperty.class)) { + try { + field.setAccessible(true); + String name = field.getName(); + Object value = field.get(this); + Class fieldType = field.getType(); + + // Following block is to manage toString method to print each resolver parameters in One Line + // if field name contains pip it means that currentResolver is Python and this field should be in Python + if (!name.toLowerCase().contains("pip")) { + String regex = "([a-z]+)"; + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(name); + if (m.find()) { + newResolver = m.group(); + if (resolversList.contains(newResolver.toLowerCase())) { + if (currentResolver == null || !currentResolver.equals(newResolver)) { + if (currentResolver != null) { + result.append(Constants.CLOSE_CURLY_BRACKET); + result.append(Constants.NEW_LINE); + } + result.append(newResolver); + result.append(": {"); + currentResolver = newResolver; + } + } else { + // This block for parameters that are not belong to each resolver + result.append(field.getName() + Constants.EQUALS + value + Constants.NEW_LINE); + continue; + } + } + } + if (value == null) { + result.append(field.getName() + Constants.EQUALS + Constants.EMPTY_STRING + Constants.COMMA + Constants.WHITESPACE); + } else { + if (fieldType.isArray()) { + result.append(field.getName() + Constants.EQUALS + Arrays.toString((Object[]) value) + Constants.COMMA + Constants.WHITESPACE); + } else { + result.append(field.getName() + Constants.EQUALS + value + Constants.COMMA + Constants.WHITESPACE); + } + } + } catch (IllegalAccessException e) { + logger.debug("Failed in Resolvers Configuration parsing toString - {}. Exception: {}", e.getMessage(), e.getStackTrace()); + } + } + } + result.append("}" + Constants.NEW_LINE); + return result.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/configuration/ScmConfiguration.java b/src/main/java/org/whitesource/fs/configuration/ScmConfiguration.java new file mode 100644 index 0000000..28ed417 --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/ScmConfiguration.java @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +/** + * Author: eugen.horovitz + */ +public class ScmConfiguration { + + /* --- Constructors --- */ + + @JsonCreator + public ScmConfiguration( + @JsonProperty(SCM_TYPE_PROPERTY_KEY) String type, + @JsonProperty(SCM_USER_PROPERTY_KEY) String user, + @JsonProperty(SCM_PASS_PROPERTY_KEY) String pass, + @JsonProperty(SCM_PPK_PROPERTY_KEY) String ppk, + @JsonProperty(SCM_URL_PROPERTY_KEY) String url, + @JsonProperty(SCM_BRANCH_PROPERTY_KEY) String branch, + @JsonProperty(SCM_TAG_PROPERTY_KEY) String tag, + @JsonProperty(SCM_REPOSITORIES_FILE) String repositoriesPath, + @JsonProperty(SCM_NPM_INSTALL) boolean npmInstall, + @JsonProperty(SCM_NPM_INSTALL_TIMEOUT_MINUTES) int npmInstallTimeoutMinutes) { + this.type = type; + this.user = user; + this.pass = pass; + this.ppk = ppk; + this.url = url; + this.branch = branch; + this.tag = tag; + + //defaults + this.repositoriesPath = repositoriesPath; + this.npmInstall = npmInstall; + this.npmInstallTimeoutMinutes = npmInstallTimeoutMinutes; + } + + /* --- Members --- */ + + private String type; + private String user; + private String pass; + private String ppk; + private String url; + private String branch; + private String tag; + + private String repositoriesPath; + private boolean npmInstall; + private int npmInstallTimeoutMinutes; + + /* --- Properties --- */ + + @JsonProperty("scm.type") + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @JsonProperty("scm.user") + public String getUser() { + return user; + } + + + @JsonProperty(SCM_PASS_PROPERTY_KEY) + public String getPass() { + return pass; + } + + @JsonProperty(SCM_PPK_PROPERTY_KEY) + public String getPpk() { + return ppk; + } + + @JsonProperty(SCM_URL_PROPERTY_KEY) + public String getUrl() { + return url; + } + + @JsonProperty(SCM_BRANCH_PROPERTY_KEY) + public String getBranch() { + return branch; + } + + @JsonProperty(SCM_TAG_PROPERTY_KEY) + public String getTag() { + return tag; + } + + @JsonProperty(SCM_REPOSITORIES_FILE) + public String getRepositoriesPath() { + return repositoriesPath; + } + + @JsonProperty(SCM_NPM_INSTALL) + public boolean isNpmInstall() { + return npmInstall; + } + + @JsonProperty(SCM_NPM_INSTALL_TIMEOUT_MINUTES) + public int getNpmInstallTimeoutMinutes() { + return npmInstallTimeoutMinutes; + } +} \ No newline at end of file diff --git a/src/main/java/org/whitesource/fs/configuration/ScmRepositoriesParser.java b/src/main/java/org/whitesource/fs/configuration/ScmRepositoriesParser.java new file mode 100644 index 0000000..ed2fc7c --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/ScmRepositoriesParser.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import org.apache.commons.io.IOUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * Author: eugen.horovitz + */ +public class ScmRepositoriesParser { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(ScmRepositoriesParser.class); + public static final String URL = "url"; + public static final String BRANCH = "branch"; + public static final String SCM_REPOSITORIES = "scmRepositories"; + + /* --- Static methods --- */ + + public Collection parseRepositoriesFile(String fileName, String scmType, String scmPpk, String scmUser, String scmPassword) { + try (InputStream is = new FileInputStream(fileName)) { + String jsonText = IOUtils.toString(is); + JSONObject json = new JSONObject(jsonText); + JSONArray arr = json.getJSONArray(SCM_REPOSITORIES); + + List configurationList = new LinkedList<>(); + arr.forEach(scm -> { + JSONObject obj = (JSONObject) scm; + String url = obj.getString(URL); + String branch = obj.getString(BRANCH); + String tag = obj.getString(Constants.TAG); + configurationList.add(new ScmConfiguration(scmType, scmUser, scmPassword, scmPpk, url, branch, tag, + null, false ,1)); + }); + + return configurationList; + } catch (FileNotFoundException e) { + logger.error("file Not Found: {}", fileName); + } catch (IOException e) { + logger.error("error getting file : {}", e.getMessage()); + } + return null; + } +} diff --git a/src/main/java/org/whitesource/fs/configuration/SenderConfiguration.java b/src/main/java/org/whitesource/fs/configuration/SenderConfiguration.java new file mode 100644 index 0000000..fe10ecb --- /dev/null +++ b/src/main/java/org/whitesource/fs/configuration/SenderConfiguration.java @@ -0,0 +1,180 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.fs.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.whitesource.agent.client.ClientConstants; +import org.whitesource.agent.utils.WsStringUtils; +import org.whitesource.fs.FSAConfigProperty; + +import static org.whitesource.agent.ConfigPropertyKeys.*; + +public class SenderConfiguration { + + @FSAConfigProperty + private final boolean checkPolicies; + @FSAConfigProperty + private final String serviceUrl; + private final String proxyHost; + private final int connectionTimeOut; + private final int proxyPort; + private final String proxyUser; + private final String proxyPassword; + @FSAConfigProperty + private final boolean forceCheckAllDependencies; + @FSAConfigProperty + private final boolean forceUpdate; + @FSAConfigProperty + private final boolean forceUpdateFailBuildOnPolicyViolation; + @FSAConfigProperty + private final String updateTypeValue; + private boolean enableImpactAnalysis; + private final boolean ignoreCertificateCheck; + private final int connectionRetries; + private final int connectionRetriesIntervals; + private final boolean sendLogsToWss; + @FSAConfigProperty + private final boolean updateInventory; + + public SenderConfiguration( + @JsonProperty(CHECK_POLICIES_PROPERTY_KEY) boolean checkPolicies, + @JsonProperty(ClientConstants.SERVICE_URL_KEYWORD) String serviceUrl, + @JsonProperty(ClientConstants.CONNECTION_TIMEOUT_KEYWORD) int connectionTimeOut, + + @JsonProperty(PROXY_HOST_PROPERTY_KEY) String proxyHost, + @JsonProperty(PROXY_PORT_PROPERTY_KEY) int proxyPort, + @JsonProperty(PROXY_USER_PROPERTY_KEY) String proxyUser, + @JsonProperty(PROXY_PASS_PROPERTY_KEY) String proxyPassword, + + @JsonProperty(FORCE_CHECK_ALL_DEPENDENCIES) boolean forceCheckAllDependencies, + @JsonProperty(FORCE_UPDATE) boolean forceUpdate, + @JsonProperty(FORCE_UPDATE_FAIL_BUILD_ON_POLICY_VIOLATION) boolean forceUpdateFailBuildOnPolicyViolation, + @JsonProperty(UPDATE_TYPE) String updateTypeValue, + @JsonProperty(ENABLE_IMPACT_ANALYSIS) boolean enableImpactAnalysis, + @JsonProperty(IGNORE_CERTIFICATE_CHECK) boolean ignoreCertificateCheck, + @JsonProperty(CONNECTION_RETRIES) int connectionRetries, + @JsonProperty(CONNECTION_RETRIES_INTERVALS) int connectionRetriesIntervals, + @JsonProperty(SEND_LOGS_TO_WSS) boolean sendLogsToWss, + @JsonProperty(UPDATE_INVENTORY) boolean updateInventory){ + this.checkPolicies = checkPolicies; + this.serviceUrl = serviceUrl; + this.proxyHost = proxyHost; + this.connectionTimeOut = connectionTimeOut; + this.proxyPort = proxyPort; + this.proxyUser = proxyUser; + this.proxyPassword = proxyPassword; + this.forceCheckAllDependencies = forceCheckAllDependencies; + this.forceUpdate = forceUpdate; + this.forceUpdateFailBuildOnPolicyViolation = forceUpdateFailBuildOnPolicyViolation; + this.updateTypeValue = updateTypeValue; + this.enableImpactAnalysis = enableImpactAnalysis; + this.ignoreCertificateCheck = ignoreCertificateCheck; + this.connectionRetries = connectionRetries; + this.connectionRetriesIntervals = connectionRetriesIntervals; + this.sendLogsToWss = sendLogsToWss; + this.updateInventory = updateInventory; + } + + @JsonProperty(ClientConstants.SERVICE_URL_KEYWORD) + public String getServiceUrl() { + return serviceUrl; + } + + @JsonProperty(UPDATE_TYPE) + public String getUpdateTypeValue() { + return updateTypeValue; + } + + @JsonProperty(CHECK_POLICIES_PROPERTY_KEY) + public boolean isCheckPolicies() { + return checkPolicies; + } + + @JsonProperty(PROXY_HOST_PROPERTY_KEY) + public String getProxyHost() { + return proxyHost; + } + + @JsonProperty(ClientConstants.CONNECTION_TIMEOUT_KEYWORD) + public int getConnectionTimeOut() { + return connectionTimeOut; + } + + @JsonProperty(CONNECTION_RETRIES) + public int getConnectionRetries(){ + return connectionRetries; + } + + @JsonProperty(CONNECTION_RETRIES_INTERVALS) + public int getConnectionRetriesIntervals(){ + return connectionRetriesIntervals; + } + + @JsonProperty(PROXY_PORT_PROPERTY_KEY) + public int getProxyPort() { + return proxyPort; + } + + @JsonProperty(PROXY_USER_PROPERTY_KEY) + public String getProxyUser() { + return proxyUser; + } + + @JsonProperty(PROXY_PASS_PROPERTY_KEY) + public String getProxyPassword() { + return proxyPassword; + } + + @JsonProperty(FORCE_CHECK_ALL_DEPENDENCIES) + public boolean isForceCheckAllDependencies() { + return forceCheckAllDependencies; + } + + @JsonProperty(FORCE_UPDATE) + public boolean isForceUpdate() { + return forceUpdate; + } + + @JsonProperty(FORCE_UPDATE_FAIL_BUILD_ON_POLICY_VIOLATION) + public boolean isForceUpdateFailBuildOnPolicyViolation() { + return forceUpdateFailBuildOnPolicyViolation; + } + + @JsonProperty(ENABLE_IMPACT_ANALYSIS) + public boolean isEnableImpactAnalysis() { + return enableImpactAnalysis; + } + + @JsonProperty(IGNORE_CERTIFICATE_CHECK) + public boolean isIgnoreCertificateCheck() { + return ignoreCertificateCheck; + } + + @JsonProperty(SEND_LOGS_TO_WSS) + public boolean isSendLogsToWss(){ return sendLogsToWss; } + + @JsonProperty(UPDATE_INVENTORY) + public boolean isUpdateInventory() { + return updateInventory; + } + + public void setEnableImpactAnalysis(boolean enableImpactAnalysis) { this.enableImpactAnalysis = enableImpactAnalysis; } + + @Override + public String toString() { + return WsStringUtils.toString(this); + } +} diff --git a/src/main/java/org/whitesource/scm/GitConnector.java b/src/main/java/org/whitesource/scm/GitConnector.java new file mode 100644 index 0000000..d3af602 --- /dev/null +++ b/src/main/java/org/whitesource/scm/GitConnector.java @@ -0,0 +1,114 @@ +package org.whitesource.scm; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.dircache.InvalidPathException; +import org.eclipse.jgit.transport.*; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.File; + +/** + * Connector for Git repositories. + * + * @author tom.shapira + */ +public class GitConnector extends ScmConnector { + + /* --- Static members --- */ + + private Logger logger = LoggerFactory.getLogger(GitConnector.class); + + /* --- Constructors --- */ + + public GitConnector(String privateKey, String username, String password, String url, String branch, String tag) { + super(username, password, url, branch, tag, privateKey); + } + + /* --- Overridden methods --- */ + + @Override + protected File cloneRepository(File dest) { + Git git = null; + try { + // set branch name + String branchName = MASTER;; + String branch = getBranch(); + if (StringUtils.isNotBlank(branch)) { + branchName = branch; + } + String tag = getTag(); + if (StringUtils.isNotBlank(tag)) { + branchName = tag; + } + + CloneCommand cloneCommand = Git.cloneRepository(); + + // use private key if available + final String privateKey = getPrivateKey(); + if (StringUtils.isNotBlank(privateKey)) { + final SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() { + @Override + protected void configure(OpenSshConfig.Host host, Session session) { + // set password if available + String password = getPassword(); + if (StringUtils.isNotBlank(password)) { + session.setPassword(password); + } + } + + @Override + protected JSch createDefaultJSch(FS fs) throws JSchException { + JSch defaultJSch = super.createDefaultJSch(fs); + defaultJSch.addIdentity(privateKey); + return defaultJSch; + } + }; + cloneCommand.setTransportConfigCallback(new TransportConfigCallback() { + @Override + public void configure(Transport transport) { + if( transport instanceof SshTransport ) { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(sshSessionFactory); + } else { + logger.warn("you are not using ssh protocol while using scm.ppk"); + } + } + }); + cloneCommand.setCredentialsProvider(new passphraseCredentialsProvider(getPassword())); + } else { + if (getUrlName() != null && getPassword() != null) { + cloneCommand.setCredentialsProvider(new UsernamePasswordCredentialsProvider(getUsername(), getPassword())); + } + } + + // clone repository + git = cloneCommand.setURI(getUrl()) + .setBranch(branchName) + .setDirectory(dest) + .call(); + } catch (InvalidPathException e) { + logger.warn("Error cloning git repository: {}", e.getMessage()); + } catch (GitAPIException e) { + logger.warn("Error processing git repository: {}", e.getMessage()); + } finally { + if (git != null) { + git.close(); + } + } + return dest; + } + + @Override + public ScmType getType() { + return ScmType.GIT; + } +} diff --git a/src/main/java/org/whitesource/scm/MercurialConnector.java b/src/main/java/org/whitesource/scm/MercurialConnector.java new file mode 100644 index 0000000..33910ee --- /dev/null +++ b/src/main/java/org/whitesource/scm/MercurialConnector.java @@ -0,0 +1,48 @@ +package org.whitesource.scm; + +import com.aragost.javahg.BaseRepository; +import com.aragost.javahg.Repository; +import com.aragost.javahg.RepositoryConfiguration; +import com.aragost.javahg.commands.BranchCommand; +import org.apache.commons.lang.StringUtils; + +import java.io.File; + +/** + * Connector for Mercurial (hg) repositories. + * + * @author tom.shapira + */ +public class MercurialConnector extends ScmConnector { + + /* --- Constructors --- */ + + public MercurialConnector(String username, String password, String url, String branch, String tag) { + super(username, password, url, branch, tag, null); + } + + /* --- Overridden methods --- */ + + @Override + protected File cloneRepository(File dest) { + BaseRepository repo = Repository.clone(RepositoryConfiguration.DEFAULT, dest, getUrl()); + + // set branch name + String branchName = MASTER;; + String branch = getBranch(); + if (StringUtils.isNotBlank(branch)) { + branchName = branch; + } + String tag = getTag(); + if (StringUtils.isNotBlank(tag)) { + branchName = tag; + } + BranchCommand.on(repo).set(branchName); + return dest; + } + + @Override + public ScmType getType() { + return ScmType.MERCURIAL; + } +} diff --git a/src/main/java/org/whitesource/scm/ScmConnector.java b/src/main/java/org/whitesource/scm/ScmConnector.java new file mode 100644 index 0000000..bb39d84 --- /dev/null +++ b/src/main/java/org/whitesource/scm/ScmConnector.java @@ -0,0 +1,147 @@ +package org.whitesource.scm; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.Constants; +import org.whitesource.agent.TempFolders; +import org.whitesource.agent.utils.FilesUtils; +import org.whitesource.agent.utils.LoggerFactory; + +import java.io.File; +import java.nio.file.Paths; + +/** + * This class holds all components for connecting to repositories using git/svm/mercurial protocol. + * + * @author tom.shapira + */ +public abstract class ScmConnector { + + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(ScmConnector.class); + + public static final String MASTER = "master"; + + /* --- Members --- */ + + private final String username; + private final String password; + private final String url; + private final String branch; + private final String tag; + private final String privateKey; + private File cloneDirectory; + + /* --- Constructors --- */ + + protected ScmConnector(String username, String password, String url, String branch, String tag, String privateKey) { + this.username = username; + this.password = password; + this.url = url; + this.branch = branch; + this.tag = tag; + this.privateKey = privateKey; + } + + /* --- Static methods --- */ + + public static ScmConnector create(String scmType, String url, String privateKey, String username, String password, String branch, String tag) { + ScmConnector scmConnector = null; + if (StringUtils.isNotBlank(scmType)) { + ScmType type = ScmType.getValue(scmType); + if (type == null) { + throw new IllegalArgumentException("Invalid scm type, please select git / svn / mercurial"); + } + + if (StringUtils.isBlank(url)) { + throw new IllegalArgumentException("No scm link provided"); + } + + switch (type) { + case GIT: + scmConnector = new GitConnector(privateKey, username, password, url, branch, tag); + break; + case SVN: + scmConnector = new SvnConnector(username, password, url, branch, tag); + break; + case MERCURIAL: + scmConnector = new MercurialConnector(username, password, url, branch, tag); + break; + default: throw new IllegalArgumentException("Unsupported scm type"); + } + } + return scmConnector; + } + + /* --- Public methods --- */ + + /** + * Clones the given repository. + * + * @return The folder in which the specific branch/tag resides. + */ + public File cloneRepository() { + String scmTempFolder = new FilesUtils().createTmpFolder(false, TempFolders.UNIQUE_SCM_TEMP_FOLDER); + cloneDirectory = new File(scmTempFolder, getType().toString().toLowerCase() + Constants.UNDERSCORE + + getUrlName() + Constants.UNDERSCORE + getBranch()); + FilesUtils.deleteDirectory(cloneDirectory); // delete just in case it's not empty + + logger.info("Cloning repository {} ...this may take a few minutes", getUrl()); + File branchDirectory = cloneRepository(cloneDirectory); + return branchDirectory; + } + + public void deleteCloneDirectory() { + new TempFolders().deleteTempFoldersHelper(Paths.get(System.getProperty("java.io.tmpdir"), TempFolders.UNIQUE_SCM_TEMP_FOLDER).toString()); + } + + /* --- Abstract methods --- */ + + protected abstract File cloneRepository(File dest); + + public abstract ScmType getType(); + + /* --- Private methods --- */ + +// private void deleteDirectory(File directory) { +// if (directory != null) { +// try { +// FileUtils.forceDelete(directory); +// } catch (IOException e) { +// // do nothing +// } +// } +// } + + /* --- Getters / Setters --- */ + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getUrl() { + return url; + } + + public String getUrlName() { + return this.url.substring(this.url.lastIndexOf('/') + 1, this.url.length()); + } + + public String getBranch() { + return branch; + } + + public String getTag() { + return tag; + } + + public String getPrivateKey() { + return privateKey; + } +} diff --git a/src/main/java/org/whitesource/scm/ScmType.java b/src/main/java/org/whitesource/scm/ScmType.java new file mode 100644 index 0000000..4f5c57a --- /dev/null +++ b/src/main/java/org/whitesource/scm/ScmType.java @@ -0,0 +1,33 @@ +package org.whitesource.scm; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enum for scm types. + * + * @author tom.shapira + */ +public enum ScmType { + + GIT, + SVN, + MERCURIAL; + + /* --- Static members --- */ + + private static final Map scmTypeMap; + + static { + scmTypeMap = new HashMap(); + scmTypeMap.put("git", ScmType.GIT); + scmTypeMap.put("svn", ScmType.SVN); + scmTypeMap.put("mercurial", ScmType.MERCURIAL); + } + + /* --- Static methods --- */ + + public static ScmType getValue(String value) { + return scmTypeMap.get(value); + } +} diff --git a/src/main/java/org/whitesource/scm/SvnConnector.java b/src/main/java/org/whitesource/scm/SvnConnector.java new file mode 100644 index 0000000..48d9677 --- /dev/null +++ b/src/main/java/org/whitesource/scm/SvnConnector.java @@ -0,0 +1,89 @@ +package org.whitesource.scm; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNRevision; +import org.tmatesoft.svn.core.wc.SVNUpdateClient; +import org.tmatesoft.svn.core.wc.SVNWCUtil; +import org.tmatesoft.svn.core.wc2.SvnCheckout; +import org.tmatesoft.svn.core.wc2.SvnOperationFactory; +import org.tmatesoft.svn.core.wc2.SvnTarget; + +import java.io.File; + +/** + * Connector for SVN repositories. + * + * @author tom.shapira + */ +public class SvnConnector extends ScmConnector { + + /* --- Static members --- */ + + private final Logger logger = LoggerFactory.getLogger(SvnConnector.class); + + private static final String URL_BRANCHES = "/branches/"; + private static final String URL_TAGS = "/tags/"; + private static final String URL_TRUNK = "/trunk"; + private static final String TRUNK = "trunk"; + + /* --- Constructors --- */ + + public SvnConnector(String username, String password, String url, String branch, String tag) { + super(username, password, url, branch, tag, null); + } + + /* --- Overridden methods --- */ + + @Override + protected File cloneRepository(File dest) { + // build url + String url = getUrl(); + String branch = getBranch(); + String tag = getTag(); + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(url); + if (StringUtils.isNotBlank(branch)) { + if (branch.equalsIgnoreCase(TRUNK)) { + if (!url.endsWith(TRUNK)) { + urlBuilder.append(URL_TRUNK); + } + } else { + urlBuilder.append(URL_BRANCHES); + urlBuilder.append(branch); + } + } else if (StringUtils.isNotBlank(tag)) { + urlBuilder.append(URL_TAGS); + urlBuilder.append(tag); + } + + // setup svn client + SVNClientManager clientManager = SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true), getUsername(), getPassword()); + SVNUpdateClient updateClient = clientManager.getUpdateClient(); + updateClient.setIgnoreExternals(false); + + final SvnOperationFactory svnOperationFactory = new SvnOperationFactory(); + try { + final SvnCheckout checkout = svnOperationFactory.createCheckout(); + checkout.setSingleTarget(SvnTarget.fromFile(dest)); + checkout.setSource(SvnTarget.fromURL(SVNURL.parseURIEncoded(urlBuilder.toString()))); + checkout.setRevision(SVNRevision.HEAD); + checkout.run(); + } catch (SVNException e) { + logger.error("error during checkout: {}", e.getMessage()); + } finally { + svnOperationFactory.dispose(); + } + + return dest; + } + + @Override + public ScmType getType() { + return ScmType.SVN; + } +} diff --git a/src/main/java/org/whitesource/scm/passphraseCredentialsProvider.java b/src/main/java/org/whitesource/scm/passphraseCredentialsProvider.java new file mode 100644 index 0000000..54d9382 --- /dev/null +++ b/src/main/java/org/whitesource/scm/passphraseCredentialsProvider.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.whitesource.scm; + +import org.eclipse.jgit.errors.UnsupportedCredentialItem; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.URIish; + +import java.util.Arrays; + +/** + * {@link CredentialsProvider} that holds passphrase for SSH connection. + * + * Implementation taken from {@link org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider} and modified + * to support SSH connections. + */ +public class passphraseCredentialsProvider extends CredentialsProvider { + + /* --- Members --- */ + + private char[] password; + + /** + * Initialize the provider with a single username and password. + * + * @param password password according to credentials provider + */ + public passphraseCredentialsProvider(String password) { + this(password.toCharArray()); + } + + /** + * Initialize the provider with a single username and password. + * + * @param password password according to credentials provider + */ + public passphraseCredentialsProvider(char[] password) { + this.password = password; + } + + @Override + public boolean isInteractive() { + return false; + } + + @Override + public boolean supports(CredentialItem... items) { + for (CredentialItem i : items) { + if (i instanceof CredentialItem.Username) + continue; + + else if (i instanceof CredentialItem.Password) + continue; + + else + return false; + } + return true; + } + + @Override + public boolean get(URIish uri, CredentialItem... items) + throws UnsupportedCredentialItem { + for (CredentialItem i : items) { + if (i instanceof CredentialItem.Password) { + ((CredentialItem.Password) i).setValue(password); + continue; + } + if (i instanceof CredentialItem.StringType) { + if (i.getPromptText().startsWith("Passphrase for ")) { //$NON-NLS-1$ + ((CredentialItem.StringType) i).setValue(new String( + password)); + continue; + } + } + throw new UnsupportedCredentialItem(uri, i.getClass().getName() + + ":" + i.getPromptText()); //$NON-NLS-1$ + } + return true; + } + + /** Destroy the saved username and password.. */ + public void clear() { + if (password != null) { + Arrays.fill(password, (char) 0); + password = null; + } + } +} diff --git a/src/main/java/org/whitesource/web/FsaVerticle.java b/src/main/java/org/whitesource/web/FsaVerticle.java new file mode 100644 index 0000000..c95621b --- /dev/null +++ b/src/main/java/org/whitesource/web/FsaVerticle.java @@ -0,0 +1,206 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.web; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.JksOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.SystemUtils; +import org.slf4j.Logger; +import org.whitesource.agent.utils.LoggerFactory; +import org.whitesource.agent.Constants; +import org.whitesource.agent.utils.CommandLineProcess; +import org.whitesource.fs.*; +import org.whitesource.fs.configuration.ConfigurationSerializer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Properties; +import java.util.UUID; + +import static org.whitesource.agent.ConfigPropertyKeys.ENDPOINT_PORT; + +/** + * Blocking Verticle that does the work on top of the FSA + */ +public class FsaVerticle extends AbstractVerticle { + + private final Logger logger = LoggerFactory.getLogger(FsaVerticle.class); + public static final String API_ANALYZE = "/analyze"; + public static final String API_SEND = "/send"; + public static final String HOME = "/"; + public static final String WELCOME_MESSAGE = "

File system agent is up and running

"; + public static final String CONFIGURATION = "configuration"; + public static final String KEYSTORE_JKS = "keystore.jks"; + private FSAConfiguration localFsaConfiguration; + + @Override + public void start(Future fut) { + Router router = Router.router(vertx); + // add a handler which sets the request body on the RoutingContext. + router.route().handler(BodyHandler.create()); + + // expose a POST method endpoint on the URI: /analyze + router.post(API_ANALYZE).blockingHandler(this::analyze); + + // expose a POST method endpoint on the URI: /send + router.post(API_SEND).blockingHandler(this::send); + + router.get(HOME).handler(this::welcome); + + String config = config().getString(CONFIGURATION); + if (config == null) { + localFsaConfiguration = new FSAConfiguration(); + } else { + localFsaConfiguration = ConfigurationSerializer.getFromString(config, FSAConfiguration.class, false); + } + + String certificate = localFsaConfiguration.getEndpoint().getCertificate(); + String pass = localFsaConfiguration.getEndpoint().getPass(); + + if (StringUtils.isEmpty(certificate) || StringUtils.isEmpty(pass) && localFsaConfiguration.getEndpoint().isSsl()) { + certificate = KEYSTORE_JKS; + pass = UUID.randomUUID().toString(); + generateCertificateAndPass(certificate, pass); + } + + // Create Http server and pass the 'accept' method to the request handler + vertx.createHttpServer(new HttpServerOptions().setSsl(localFsaConfiguration.getEndpoint().isSsl()).setKeyStoreOptions(new JksOptions() + .setPath(certificate) + .setPassword(pass) + )).requestHandler(router::accept). + listen(config().getInteger(ENDPOINT_PORT, localFsaConfiguration.getEndpoint().getPort()), + result -> { + if (result.succeeded()) { + logger.info("Http server completed.."); + fut.complete(); + } else { + fut.fail(result.cause()); + logger.warn("Http server failed.."); + } + } + ); + } + + private boolean generateCertificateAndPass(String keystoreName, String password) { + String keyToolPath = Paths.get(System.getProperty("java.home"), "bin", "keytool").toString(); + String[] params = new String[]{keyToolPath, "-genkey", "-alias", "replserver", "-keyalg", "RSA", "-keystore", keystoreName, "-dname", + "\"CN=author, OU=Whitesource, O=WS, L=Location, S=State, C=US\"", "-storepass", password, "-keypass", password}; + + if (SystemUtils.IS_OS_LINUX) { + params = new String[]{keyToolPath, "-genkey", "-alias", "replserver", "-keyalg", "RSA", "-keystore", keystoreName, "-dname", + "CN=author, OU=Whitesource, O=WS, L=Location, S=State, C=US", "-storepass", password, "-keypass", password}; + } + + CommandLineProcess commandLineProcess = new CommandLineProcess(System.getProperty("user.dir"), params); + try { + if (Files.exists(Paths.get(keystoreName))) { + Files.delete(Paths.get(keystoreName)); + } + logger.debug("Running: " + String.join(Constants.WHITESPACE, params)); + commandLineProcess.executeProcess(); + if (commandLineProcess.isErrorInProcess()) { + logger.error("Error creating self signed certificate"); + return false; + } else { + logger.info("Self signed certificate created"); + return true; + } + } catch (IOException e) { + logger.debug("Error creating certificate" + e); + logger.error("Error creating self signed certificate"); + return false; + } + } + + private void send(RoutingContext context) { + vertx.executeBlocking(future -> { + ProjectsDetails resultProjects = getProjects(context, true); + future.complete(resultProjects); + }, false, res -> { + if (res.failed()) { + logger.error("error running blocking request:", res.cause().getMessage()); + } else { + ProjectsDetails resultProjects = (ProjectsDetails)res.result(); + ResultDto resultDto = new ResultDto(resultProjects.getDetails(), resultProjects.getStatusCode()); + handleResponse(context, resultDto); + } + }); + + } + + public void analyze(RoutingContext context) { + ProjectsDetails result = getProjects(context, false); + ResultDto resultDto = new ResultDto(new ProjectsDetails(result.getProjects(),result.getStatusCode(),result.getDetails()), result.getStatusCode()); + handleResponse(context, resultDto); + } + + private void handleResponse(RoutingContext context, ResultDto resultDto) { + String result = null; + try { + result = new ObjectMapper().writeValueAsString(resultDto); + } catch (JsonProcessingException e) { + logger.error("Error writing json:", e); + context.response().end("Scanning has failed"); + } + context.response().end(result); + } + + private void welcome(RoutingContext context) { + context.response().end(WELCOME_MESSAGE); + } + + private ProjectsDetails getProjects(RoutingContext context, boolean shouldSend) { + final FSAConfiguration webFsaConfiguration = ConfigurationSerializer.getFromString(context.getBodyAsString(), FSAConfiguration.class, false); + + if (webFsaConfiguration != null) { + HashMap result = ConfigurationSerializer.getFromString(context.getBodyAsString(), HashMap.class, false); + FSAConfiguration mergedFsaConfiguration = mergeConfigurations(localFsaConfiguration, result); + + Main main = new Main(); + return main.scanAndSend(mergedFsaConfiguration, shouldSend); + } + return new ProjectsDetails(new ArrayList<>(), StatusCode.ERROR, "Error parsing the request"); + } + + private FSAConfiguration mergeConfigurations(FSAConfiguration baseFsaConfiguration, HashMap parameterMap) { + Properties properties = ConfigurationSerializer.getAsProperties(parameterMap); + FSAConfiguration.ignoredWebProperties.forEach(property->{ + if (properties.containsKey(property)) { + logger.info("Property "+ property +" will be ignored"); + properties.remove(property); + } + }); + + Properties propertiesLocal = ConfigurationSerializer.getAsProperties(baseFsaConfiguration); + + FSAConfigProperties merged = new FSAConfigProperties(); + merged.putAll(propertiesLocal); + merged.putAll(properties); + + return new FSAConfiguration(merged); + } +} diff --git a/src/main/java/org/whitesource/web/ResultDto.java b/src/main/java/org/whitesource/web/ResultDto.java new file mode 100644 index 0000000..14bce5b --- /dev/null +++ b/src/main/java/org/whitesource/web/ResultDto.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2014 WhiteSource Ltd. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.whitesource.web; + +public class ResultDto { + + private T result; + private V details; + + public ResultDto() { + + } + + public ResultDto(T result, V details) { + this.result = result; + this.details = details; + } + + public T getResult() { + return result; + } + + public void setResult(T result) { + this.result = result; + } + + public V getDetails() { + return details; + } + + public void setDetails(V details) { + this.details = details; + } +} diff --git a/src/main/resources/copyDependenciesTask.txt b/src/main/resources/copyDependenciesTask.txt new file mode 100644 index 0000000..915fec1 --- /dev/null +++ b/src/main/resources/copyDependenciesTask.txt @@ -0,0 +1,10 @@ + +task copyDependencies(type: Copy) { + from configurations.runtime + from configurations.testCompile + from configurations.testRuntime + from configurations.compileOnly + from configurations.testCompileOnly + from configurations.compile + into "lib" +} \ No newline at end of file diff --git a/src/main/resources/helpContent.txt b/src/main/resources/helpContent.txt new file mode 100644 index 0000000..a9ac418 --- /dev/null +++ b/src/main/resources/helpContent.txt @@ -0,0 +1,62 @@ +Usage: java -jar path/to/jarfile [parameters] [args...] + +where parameters include: + -c Configuration file name (including file path) + Optional - the default filename is whitesource-fs-agent.config. + -d Comma separated list of directories and / or files to scan + Optional - the default directory is the directory where the jar file is located. + -f File list path + To avoid a long command line string, + use a text file with folders and files separated by new lines + Optional. + -apiKey Unique identifier of the organization + It can be retrieved from the admin page in your WhiteSource account. + Required. + -project + Name of the project to update. + Required - unless a valid projectToken is specified. + -projectVersion + Version of the project + Optional - value is only read if projectName is specified. + -projectToken + Unique identifier of the product to update + It can be retrieved from the 'Integrate' page in your WhiteSource account. + Optional - if both projectToken and projectName are empty, projectName will default to "My Project". + -product Name of the product to update + Optional - if empty, an existing WhiteSource product will attempt to be matched by the value of productToken + If productToken is empty or invalid, it defaults to "My Product". + -productVersion + Version of the product and project to update + This overrides the project version + Optional - only evaluated if productName is specified. + -productToken + Unique identifier of the product to update + It can be retrieved from the 'Integrate' page in your WhiteSource account + Optional - if both productToken and productName are empty, productName will default to "My Product". + -proxy.host + Proxy hostname + Optional. + -proxy.port + Proxy port number + Optional. + -proxy.user + Proxy username + Optional. + -proxy.pass + Proxy password + Optional. + -requestFiles + Comma separated list of paths to offline request files + Optional. + -projectPerFolder + Creates a project for each subfolder. The subfolder's name is used as the project name + Optional. + -updateType + Specifies whether or not the project dependencies should be removed before adding the new ones + Specify APPEND in case of adding files to an existing project + Optional. + -userKey Unique identifier of user + It can be generated from the 'Profile' page in your WhiteSource account + Optional - unless WhiteSource administrator has enabled "Enforce user level access" option. + +See https://whitesource.atlassian.net/wiki/spaces/WD/pages/33718339/File+System+Agent for more details. \ No newline at end of file diff --git a/src/main/resources/logback-FSA.xml b/src/main/resources/logback-FSA.xml new file mode 100644 index 0000000..678c114 --- /dev/null +++ b/src/main/resources/logback-FSA.xml @@ -0,0 +1,47 @@ + + + + + + + loggerName + + + + appenderName + + + + true + + + + + [%level] [%d{"yyyy-MM-dd HH:mm:ss,SSS Z"}] - %msg%n + + + + + false + + [%level] [%d{"yyyy-MM-dd HH:mm:ss,SSS Z"}[ - %msg%n + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/project.properties b/src/main/resources/project.properties new file mode 100644 index 0000000..98e89fe --- /dev/null +++ b/src/main/resources/project.properties @@ -0,0 +1,3 @@ +version=${project.version} +artifactId=${project.artifactId} +agentsVersion=${agent.api.version} \ No newline at end of file diff --git a/test_input/gradle/.gradle/4.0/fileChanges/last-build.bin b/test_input/gradle/.gradle/4.0/fileChanges/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.0/fileContent/annotation-processors.bin b/test_input/gradle/.gradle/4.0/fileContent/annotation-processors.bin new file mode 100644 index 0000000000000000000000000000000000000000..81d4d055dec075bcad7700a753e9af41749b8c65 GIT binary patch literal 18533 zcmeI%F$%&k6b9hfO_Ay%^ag@-5Clh|;@~bF1W)1Opx1HqHlD-_Xi8d&7jTg8L6XP+ zhLCT~E`(zGZY%UP>suWG0t5&UAV7cs0RjXF5FkK+009C72oNCfF9LOZ$S6IgD6^)S zTtZlmW0M{0`Tcc1m1(@My8naQdU4%sAI~;Fba{8iyh<}W0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNCfOMz9qY#7&|T_OKUnmGRe DP^<;5oT7+I zMUYfl7{_WrDBuYdQLxmjqA*p6j+Rm{K(1JV>2AL7!9a5}sc7@P1HW>KVirq!E6o#JljBBw331yqkm{9q(d4^T?}7c)dO0$4*Ch zJudA`McixQ0N0!~aDR|nhSygTUaTn|H_|3hfw(M@@N*|b37J9K-FUq};pdB`bcBfn=>k+2rDG0x=?lm_SrFh}j zsUy68S%%!|Vm;}ZMfmTDP8lE87UT7@rG!7t|KV2H<*OIb>v+Wz-nq?{60h1Xz}%AX zE^+%osW!(Eap|Xo_q+&Kd?4TIiT9_U@P4~)N&d^=c)Whj01vn;Z?TMBfVj+NfVY>t zqiq(PM0|cP;l_UEwGu0nD$FktKE^F4GD#lVi1|LkZPOB}6zV^YATG@(e0sCPc)N<7 zu6X?_!e^IHbl|;Ii17Lb!hPLUTvdCv5b=58gfA}`**zpUtPAsDga>keHhP#fO^vyb z@Ce!1aTD!23=v<@K{$D&C_{VnH@JraPyh-*0Vn_kpa2wr0#E=7KmjNK1)u;FfC5ke z3P1rU00p1`6o3Ly017|>C;$bZ02F`%Pyh-*0Vn_kpa2wr0#E=7{C5gC)4Rg_*x8!7 znWnAV>h_&)M_0o12R^gJ@jevA`Q+GdBPO8z<#6f8DQ_Byyy}8ckF@EY{^!Fgk8667 zVSEi46l3ZcEuq|~$gb>aho-#yvo^)AU<{@0=lIv{oEWyTzcnVa&HZLeUr)*l?T|_% zG$VkIW(=)o8waXVOO;piraciQjWAIM??J|jg~+&F%r;!4RwmwNV$KlZv4-GP@8TK$ zuaH4KWgAK2CkZ8Ia!x&V&Nz8@rLP3fxQUF>MQp>b%-Dx(Gb29Wkbi5#E>RP0tZrN6 zza%{I(tKb%}My*6Ijs!Bb4$FBkz8e5%V&~uM%RP%?fD{m|hxz*vMh-=#- zXAGr-3qDGyK|YV+S!ax-QLTq(mfg2b@zBs$0?-EU5Wd2KDrL{8*XC;YDU;140(({X ziF04087~ubdqq)EY-4G5+qKRmJEr6+OOoTP5`uJw<7NCSe~P*KK<`MDPdooSV#Z3L zc+;hieKuFop98&DQ`g|@K@6A!UC(e9=Wol%sryMKSif~&b%WKL2B({Cghu-sEl#6` z3To1~+XucFhK*!;CB1Pj(06(>`fMzuOGKXBa=!K0eZ88q*vQfe6o)y!^o+=UY4crK zTjPXF;Xbizta*%47{U7y8Pt1h<8XmxQP%AqNq>cHqi}zAEi(A@_k`JuUFbW#8B0vP z=L#u@ehX23S5v{4GM%v@8efA$&1D-0Oy^ieeUVwCw)?2gRGmT}74%+(*&k#k(AyRI zrZ*$fu*$I~B(LMRy1l@tsH6oM_xyD0DXNfd7%b0z7C5!F@~!Nq+hbx9gLOu@co{YZ zIj%Glt~QiaXDUi}M1NbcIWkXYMDSGD*vy_0HA(3j_f^=1wOiyti%*!RW1|xp2K1Ri z|9x~O-WYjjo5$k~<954!@BDy1wKAUvpSkvm-Uj;w_x_o`Cvo;S2a+~;1^jw4SNA=q tjnUY6L_g}a`d7}otJ{3skK!4FoOcJAvAWBMi_Q$xKRmZHhUwpD`~}kMOI-i} literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.0/fileHashes/fileHashes.lock b/test_input/gradle/.gradle/4.0/fileHashes/fileHashes.lock new file mode 100644 index 0000000000000000000000000000000000000000..afe4868e7133b9582f6711af839ee3485c17cf2f GIT binary patch literal 17 UcmZSnm~~S-Z-(VB1_=_;vh>_ zip!!nX-1hGT$E*Mjmv1c&UfwmZS?&Q^!(K4`+a|3pV#H{xPFHP!Kly6ODL^|(#V7W z0tg_000IagfB*srAb)i@k9Ut1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmdW?0zJ})N!qoAt@mfl^(&UxL2f%18Td9B zq|p4g@nj9ete(@^NYYg4+@0G}H(nF=$#z$rePP{~TRRIcsvFt7yWHQ>YV1#*45dOZ pP3lJ5YN8_@H(cH>d3yK6xKG{ij%q)59(42K`(x_us@YO#`~cM)lWPC~ literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.0/taskHistory/fileSnapshots.bin b/test_input/gradle/.gradle/4.0/taskHistory/fileSnapshots.bin new file mode 100644 index 0000000000000000000000000000000000000000..b857637db4f4033b9dc4cb9a49cadec46846e1ba GIT binary patch literal 24635 zcmeI44^&gv9mn(D3mj){Np&sgDvnbTr6zg#2T2*Hf&!rxLn5XSJ=O9CTF}er`-KA?!Fj4Mz@FnB7g`W0*C-2 zfCwN0hyWsh2p|H803v`0AOeU0B7g`W0*C-2@H7%g27?|82ktl1{f>Z7u0TMJ15&^i zalNwic-Im5>v(_u{e$upU1@P$cPZ4DOyKp(iNqoqUG-O}Ya{yg{B4&f&2J2c`WxT# z`gZFD6BXomm&+5x>z}mVxzbT~!o}q&;q}_Go%!#7(;N$R{Y$*Qr|s)~LCbv;p`Nmr z*Y~DI2lt+R;a}YO1-#z4C}_tAjrDm@*X8m0{TO}WlP*%+`o!SC9S2um zeirJBb9g;SmzD3`7WfT!zJS+f{;K-a)o>LJbsfg*;y-oP&6}(JJ=Etn@w&AAm$$pf z>gPDUn%8Abg7txx@t2`KZ%@B|Eb8+!bA9Te9xvzh@b07CRr7kjg*sKt>oEZpYpW%7 z&p=(@$?IB6k@aO7+XD5ZLS9e2vN|VuY|AaEFKp%Yh2CMMAAdJ>4VULIuTxWg;aamz zR0Z`nxAA&LgSPK8SvJX?&&&Nsfq3?28h{g;8@Mg{?29+w*V3v_`g;7B(Xwn>NmXoBJHXBW$AxfD+ ztuY!5YQ>G%$kc6$3&JpJ`|oCoe|S^D>q=1oHm6r3~3;hDpF1= zO`&qqpfng2W{t+6A{jZUHkeg1h8r+wIC#8&Kc?2rh#9Y5QJmIYv}^lsgkXx`KYyC$ z$j!cF!Z~@WU&zI}@Jj_3CO!FcWcu1;ul%#kx4AuEuly*tE$vC4BbHcU*XHA6m*-wP zaq04bm!|d4kq7N9*gFCN=41o&s`t2mIUPnSowjCBso;5mN=UO{gmnMox1!=e4zK`o zXhPH?9`km?nFjNTF8S8H8`(<<#ootEsAHTiF4?$E`-Ix8t6N^(b1|SW>i+Kt>Aqnk zlXKZl_Lbik7fyRVMmpm{dd=q_3@@94%a&YrsRMZS-FIeg0+j~fwhZW z2XToF+}rcT8?vo76J@l~PAAyw-GyLFf?=KuF+@{$A+c7^&zBEr4@>GE6(L$0vj4jb zwLUD;ab$6#>rgn>GW^``>T-D%k(DcoH@CN4kXql}(9$^tJ2JAoatW6>HD`e^>3zW% z>CyGSosn|uCnHU);1l0_*-xvR_4?EXalnfk?%Z!=pXafgpym%;i-*B48`;3TMpjHh zGm6#3*MWsEbuVEzzELVJp&6~}jyUs>6+Pm_ByZXzH17B>A-Z|N$dS<|DBc}@dCvvLDsj@GEANJboe>O+{ zzny9k*jiV!fqCunxO@K4sk(qg1Xi_q7|kdxW3y#)9!@7phz=73_xjB`*?qq1*KvgM zfM*Bt-$Q{pd7D`xikO_Wy!7bZ>9gBKcMDIBC@JqY?ZhFn$6V3%>D`-E!n*n^gtBQk z7eC0TV;(mMncIx*OjvVwGYG{Hb&%MJr8gw|LHF8qx`|=VF|cG zbB^%7e@Ew@meQ@e-^=;9$z`v=+C3NAbM}72nH5;8%FDMMv}7+`l2kK({g0Er-6;apHCan$Mq4-+>_QeQCKj8KVQPKC6>k3}`sAN@I{99*tmXBv00WKWE^Wfl% zM{w|+BRGzEjj+Ik<`Mp+=eY*FHUG~F<3;Q=lEUTgo&}>@a=dt;KW<+9C>;(t2?lq_ zoiv)$0$=aGzJZwowhP>6h-vavTH`HMlj}B}>WS%X3OW*7!j$_?{T;-*F@M Ao&W#< literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.0/taskHistory/taskHistory.bin b/test_input/gradle/.gradle/4.0/taskHistory/taskHistory.bin new file mode 100644 index 0000000000000000000000000000000000000000..028a91affcdae5d649c58f346c1c96e7e6003c7b GIT binary patch literal 28058 zcmeI4du&tJ8NjdOu(AOy?a-D!_NWxCRX1OVorH)^lZ@>&WFcXB0G)!;<9mgm9C*k8#~9> zc5Ej~K!Hi6pQU48f9G+}`F-E{&i7qM2})AM!pqs0q@&4Nz&);#;k*|C! zClCDLt1TnHJh#S?hZ}P8+$Wd!ICbz21b_e#00KY&2mk>f00e*l5C8%|00;m9AOHk_ z01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e#00KY&2mk>f00e*l5C8%|00_*6fRF!^ zpxwM|7sWLkRt>LW)?y_b^J%f}TC>;NLyQ!OS4-tv!-y%dHiZ(Cb=~^hao215H#Wu* zttT2D+cf&UbtitIpt!!V@ zB6*)dqjHoYRl_neiOi7IZOJK$Og@gh#`)|DV#T0{Vb!(pnLG4{KYJ*A@ypL@zW)r2 z9r)NuSIH1WXPD|pllf%gSYeqlBS>KeP0RkU#VEgSv6*SJRnBzNeqA>hV#LrJkj|$< zE4`Ifu7y88GITU}T>1My{C5985AVMWx0)ruqgSJ9@L37q*|ShHFsuhIQ!L;GiknKely!K zCU)80E1XU0xhrib^KE|9Rz#H;N#fS zd1*f9}LwY5XBW?2Fnl607MZMcRozdU<}?z0tq*tv>XKa^QoJ>o6P8PH~VR zw6iqTt7?gOv?da%He){QOEd1}>~7O8($47CbvGXyz1#oa(D`L225(28o#GhiI`GH@ zu;pjZiC}auFMC9B4cl68m{eP};dnTf)X>s&jUDqvNNqCV-a-w7wV7C{tomZQ?o(^; zvc@MwUi8+y$e$MMsMwZo#iO}LUw--1%kTN!{jS3E=&4(O(emA2pGt4LW%mb{$6o*Q zkEc8h!Zwcyi5`_35xrr3;?*s^G%_xt>bim;DQpu>?bJxva-t?r6+a8xn*FQV8d}HO0V82jY>7j0?hDgv=j+4_m2}w@4AnRJ+e5+K38a$pgV?3Cs|WFj291x41Ju%dt39+ew0VM+wt%?V^}RV5+T_jPvp(5 zN>+Fz)P{A7Fdjv@h6-v_K1wDsd2C0al+cxiilxy)nQdxB;aQK>##0uCqv^RnC?OEx z6O)6J+HG9X9BAqYh6?X1nr3b>Qg90{WgQQU;hMRG4bgM z6t|tkU_#^G5&{$Cz4GMsPiUUkPSQM=&3f&H_s_3j*elJd=p$mC3(0~~%=NqJu#cxexg zM0f(l_s2x`&66q~H(Lf3JLKTdC*MM&xR)P8;#3pPR2s@Pj3A=5=pjn^!;Wovp+URLo-F*+PyEMm|^7T%$Psql_+ zzB>O!5dO15u*zLky=*u6oRN8OOKwS{u|b`BGbu(W%9m0UN(!}ciV2YG45 z05c4YA7nhCcDR)0_W3zD`#xY(%2&~tOo+*}=Q@99Vwhw$8~;|_G{IJ3nx3SiX^58xP%n#pAenD|qpXGF=>3n0d1Y zelHDlt(6Kg@39BZ*<=4Ywx=lbzG=Se+xJSZ))eK4{cr3Wyg1&o>(uAv(AhUnUYPob zhs=A@LkGybA@j~X{DRC|sr(3L-qQr5h5i|yjAs6#ARqPfXLghR96 literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.5.1/fileContent/fileContent.lock b/test_input/gradle/.gradle/4.5.1/fileContent/fileContent.lock new file mode 100644 index 0000000000000000000000000000000000000000..9949fcaba982fa8e2f620ed05c730b1b8841b288 GIT binary patch literal 17 TcmZRUOAUCn&c4))0RmV6DXIfq literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.5.1/fileHashes/fileHashes.bin b/test_input/gradle/.gradle/4.5.1/fileHashes/fileHashes.bin new file mode 100644 index 0000000000000000000000000000000000000000..cd96afe9b3cc120b9831c198ff5088c1f4f9158c GIT binary patch literal 19347 zcmeI(X-E`N00;2#$h6c{lr{u&4K>a3rqH4?%k==2@TgqW6^S%OGp#JIvY_zH3a^wD zq0A$)(n#|xi=wp9O%kPY)y$Jpv`m}bd9R51-iP=P%+BLK?>}$nw;y-Lm0<)r%VcBz zYzBXZ7(oC65P$##AOHafKmY;|fB*y_009U<00Izz00bcLzXaOhptnJ4cfFKWW}{XL7b*Y@N#Gq6(U~&AztFwX2FC z*Kwx#*JMY=CG?6lnJ=aJH<#&r(Q>g3$#>Iyp?u6jQfF;lC>lcrMY?Lg|^c9&o{{R<7mDsE4jl`ruC6r-;w6tW(PzQtG6eST!rQ# z11D~G6brwSynyEP$S_K`xO4b~00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2yg=S?7@QFh4Njee#f~pzS`1fF#)nF zwYp(n_DOU}rEKLZZZ2cQ9=H76kXmSpvoi(BJvtBlyH5A@$s0B1iR9UQrBnRIzgVNy zer)Xg@KBtWNzBrUV2?b-eGGJlEAGSCNtgevVw;_}wP|&}3_o&jkI`sZfub?^!$;B3 z6X^3BjY(&hgq72QO%eJ(6H;3nxyGWoqM!y7UGMQ}^b?zFnWI?j0g;)J7(h zDjE#4kKZtT(0j?)S1j`L>efwlT8vjTj#}iC#&donb9P0HL6GP{fNDg;`25@i*XR!< zjaq&~?WEDxAW7TPpq>3iRXJG-tJ)|R@>U0PpWo;juSz#iNs&%H7Tg^i)#zo{2%E2H zsU&M0=Qr|2?Qb4k3$*)`R^xvxWMPgpy10h2D!(xp`u=*&NV`Q^iCOhf-pwzh@ttcZ zb@LnF;`%hUna(_Hu<|t7@D8<9UxT0|XcX05EX`Bme*a literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.5.1/fileHashes/resourceHashesCache.bin b/test_input/gradle/.gradle/4.5.1/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000000000000000000000000000000000000..3374972473ffaf040d7d28a69577e05a392e3287 GIT binary patch literal 18701 zcmeI%ze@sP9LMpaL`s4z2{lSWLdpt)ByA{Cf^2b!Tv{9o8XO`Wq@f@vDgvQ~sEB?L z(GL)UgtlzUY%v-dl?48&-v6YsFN{~+EEJa^ywJfG!xtv4hH2JN@Bgz{=Ak4y+4 zfB*srAbRUy^gzZ6TI2d&Z^w zPR?g0yHuXRE4P%N$hrI$^TW~^PXrJ^009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q7Tw&?9~r#a&n2+Tfc>w`PeS=eOe#dqJ-k zL(|{JlXW(3^<7R!(#9&+USLbvcuqRT+K1~M^BeyBdh~Qb*~oo+D}&9ghTil=e`Yz= osBE-_lO3-Kee8bGXFCWb{L04Ei0WhaK{FS6f6TnxG+By`A5Qg<-2eap literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.5.1/taskHistory/taskHistory.bin b/test_input/gradle/.gradle/4.5.1/taskHistory/taskHistory.bin new file mode 100644 index 0000000000000000000000000000000000000000..a084c806d3689f4774af4931823b07d4ed2ce668 GIT binary patch literal 23950 zcmeI44U81k9l&RH4h1ynp%PFFRs%%Qo$k)=-tH0W<+$DR4ml2cdur=6@Mh-C-rUX3 zjBjT5Zb_@q5b%S8J2<|)6ERIwYrz<>)V7I|+7iUXU{NfsDW=d^@JvgC1QQc|^RXXy zyIcz_#5Qk}*`0kK|Bv7M@BjVZTp&(SGi$%}UR3cly?9IR2mwNX5Fi8y0YZQfAOr{j zLVyq;1PB2_fDj-A2mwNX5Fi8y0Yc#ag+M2EWUA}T%zJ$H%{O`}DsVl%VSLL^{CoS_ znn`YVYhC{D2MggVH!u3+mh*0VOJlyh>DUA9Bd?!&*lq7Tl5fv{;`Z;kedL`GAOr{j zLVyq;1PB2_fDj-A2mwNX5Fi8y0YZQfAOr{jLVyq;1PB2_fDj-A2mwNX5Fi8y0YZQf zAOr{jLVyq;1PB2_fDj-A2mwNX5FiAu1c4A1I*PB8_C*h8)a(k_o7Uk9QO;_)aBr8m z0(2>2F6}AbOHnjk``)Z|$2L%VYEG@3vTgMI!pA9kKdqw_n?gWRAqxzJwSd`UvXTN) znr>Q(XtHr=Kuv<0nENg?bYvy*h-Gmo>Ihg+qH9n8<`APlZ)~02^M^T4PW`m|O2~A) zID?(Oeh6Rd?28@__aQ@5I0z%DoZ6iOeOWaW%@|zfG1uwq4orGlRDo$4!0N*Pe&Wx* zrUiVb36aSsR3LQ#)lzuRcU}Zy7c}{_E@f0``Z-=!oKH5$D>|z|%gXW2yC0}3A7%q= zD8x1^8c_LQupUG>NeaY*u>cp7WLOWxdM+FdLJ_V_Jd|Dd^Z{o%VmJ~ZQvw^uI<@9aI)^K}oJRBO$dk$0#5sQ2`d6DRk7b6yQK ztQ&Tmn5>}dmT4hiH05(l#SFVCKiS_^Sy+3CY5L*}zknDwNa**gVUi=uix$UTruOjYuGQ0-CqDS+>ofNp z|HqW0^wBCS;#g19ww5>l_}eXya3@y1{196H`MIiV3gtB&2Y-3)<&WRIYt@ciOivlIPJ`^VpQw+%vy^?VbnjxI6sBpJ@E5P_*r>ShIz@&!q%`#Bb zb}fOBj_CeEqmFuf&z62`_x2hW=Fcbep&8Hi#+SSBd?l%{4~iKJ2r8sTD)o0~(+=i> z)ruw}n1-4KRNn?e1(vL%vD<%X^=%+o*` zGHdMY7aMZ-Mmt<^QWX?cv2x7nAR8O907^ltyko%9b+xI>&eN3xv0#;Z;AzSrg#B8S zP21)r^OMW%+gKEw#U}~8u*{ER_&l;5A5N~_gRhtD>pve(s3`Lc!WBe^0mV5%yz5sn zw=?mpmE3)UcE#~+w_I>QU$t!S+gMcXZ3)%ZeZl-+s@ofMm39Q*fa7&>)1Zn`#8uoZ z`U_FU*>Of6xP-0CN*lNgL3~@|lGf%#N0M!8W@>Mn+keBon}uI6KcxJp8}{GCETHIh z0Eu1rJY^rAu&#G5L6@osChyd?{vaFhcPwdK+R@UU#N`$~B~y>iKXU-lW99xCavD_nb#nN^!gTNTu%OOV=8+Lkd?MZ}Ma%np25vS2DV`Dfz@*pFJ+ zc-gooTbDE~?u;i&_1Z%n6(c1oa3kJg@XF>L@9$MKN$)lNK`z3Tj>H?7nvq~4ZCE*6 znVa-*K@9$NjPrNPn7)F?PqihIjsDgp&6~&ed%o`++gj$;#d<&1ih}<|#J1rR6Bs+T zj6re5aUQ+EigRqa|HYNZy=5zp+b-Mp;>vTji{n?`Y5cENo@2|rUb8|m>caTHSh@CB FUjbW~HYfl9 literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.5.1/taskHistory/taskHistory.lock b/test_input/gradle/.gradle/4.5.1/taskHistory/taskHistory.lock new file mode 100644 index 0000000000000000000000000000000000000000..ec72d134017b707dc805a839c44fbddcd732843b GIT binary patch literal 17 UcmZS9NmSWy@rm^i0|a;j04TEqumAu6 literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.8.1/fileChanges/last-build.bin b/test_input/gradle/.gradle/4.8.1/fileChanges/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.8.1/fileHashes/fileHashes.bin b/test_input/gradle/.gradle/4.8.1/fileHashes/fileHashes.bin new file mode 100644 index 0000000000000000000000000000000000000000..73e454987ceeee5f3ef44d387ff82b9d85440e1b GIT binary patch literal 18597 zcmeI%F-rq66ae6~gMyTLi*S?RSddl_M~6xW*9z*?LGcF&4kAhsoW;c-A>tr7h=aS6 zxJW6u*0B!atU2zE;vaC3FOa;)3y&ncg;&G?X+{4wcbHA`j>Ls+WCr22R}{(9@RPP1wV>+SOY;IqFH)^;vi zMg5?xmoe7c`JR;k0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oU&7f#rBuBhH0vCikmnC*AtsX@7fkGj;dg4WY7oJ(_+lZZscb^LNLf eQR`itr~TLDVe931IE)>89~+&kyrXiIb^HL{e?x)* literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.8.1/fileHashes/fileHashes.lock b/test_input/gradle/.gradle/4.8.1/fileHashes/fileHashes.lock new file mode 100644 index 0000000000000000000000000000000000000000..1dbe0f8f5da202952ba0e483ad1b4db718b0618c GIT binary patch literal 17 UcmZS9to{AkOvPpn0|c-G0587-$p8QV literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.8.1/taskHistory/taskHistory.bin b/test_input/gradle/.gradle/4.8.1/taskHistory/taskHistory.bin new file mode 100644 index 0000000000000000000000000000000000000000..08275d28836cfd0f9fd010054f94d9755d1f632a GIT binary patch literal 18722 zcmeI%JxT;I6u|LBI}vvA16F&B1sMh1Qbd_TEEi=<$xfc*?8nS78PxR%R$6cC0X%|7 zu(a_6VsEEe$7N3-i2NTUdE|xg@@o?+)u?~|gqp?bEG0t#0R#|0009ILKmY**5I_I{ z1Q0*~0R#|00D=D^aOxkjP(7xxIX6jaS4wR!`a~bf{-_vFZt&w8XMM6I;;00IagfB*srAbdlyBShoI7Wvob?k;*Wi){JsBm6&juRML#f7@%H zKZN{qTm2jTybOKb33-@p?$@$67!g1K0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5J2Fv0!_P}yA)9H-R)j>p}RoP_dWNW(?-d0 xGLELPTxz9eE9K_aTt?c&c34ejN+!#-T}fqPbQGtlHga%G2H`nxH+($%$pZ{cR+az& literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.8/fileContent/fileContent.lock b/test_input/gradle/.gradle/4.8/fileContent/fileContent.lock new file mode 100644 index 0000000000000000000000000000000000000000..e625ca6aba092e86fef5f6e668bb2171a3b0a5c2 GIT binary patch literal 17 TcmZQJk(9CI{m?Xl0RmV79(@A3 literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/4.8/fileHashes/resourceHashesCache.bin b/test_input/gradle/.gradle/4.8/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000000000000000000000000000000000000..7e843d63b106145691c2503a60ff9e74eea6786b GIT binary patch literal 18701 zcmeI)&nv@m9LMo*vsMSN% zlfH@b{I)%%d_T}cENu2q1s}0tg_000IagfB*srAbm zt1nVm|GN<%@GQA(0o9^IpPp5_bN?Co(^v?bj$C$gbAQLKcH6MoUED}#-6g)dCb8%F tY%sZ&FcddhgLkI4SameMJY@;_W8UJ%%$WLP|6UvLzds~juWDOzjc<#@jSm0- literal 0 HcmV?d00001 diff --git a/test_input/gradle/.gradle/vcsWorkingDirs/gc.properties b/test_input/gradle/.gradle/vcsWorkingDirs/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/test_input/gradle/build.gradle b/test_input/gradle/build.gradle new file mode 100644 index 0000000..b3a18e3 --- /dev/null +++ b/test_input/gradle/build.gradle @@ -0,0 +1,16 @@ +group 'elad' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + compile group: 'io.netty', name: 'netty-all', version: '4.0.33.Final' + compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.6.1' +} diff --git a/test_input/gradle/build/libs/elads-1.0-SNAPSHOT.jar b/test_input/gradle/build/libs/elads-1.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..2ec0786aa8a4718d5f72a31e90d58dba39fcc3ee GIT binary patch literal 1844 zcmWIWW@h1HVBp|j5Y0O4!vF+KAOZ+Df!NnI#8KDN&rP41ApovWrlQMdQ$5o=X`lj0 zAQnMZ=E;?7qUY=O+4sz8A8%c~i@e^tTIbH3-yCFc#rVO~M^BlM3`4h-6=+yV zYHMpmfh2)g0H(+zH76%uFF7Z%xY+t@{&hPxHa1~4zcRM6GB!Rpwr5w4 z2q~;wD0%F}0ns_}QB&r;nl^JX*OQ7R&z?MaovJ!bmB~_dSdu!(^;Ie~GbL`0*CFtaq8!}-4@-STH-U|<1;nIMXrj3AC` zo#5+z*g>G}zOVhIW%AdeULW`Uc%iDm(S}3xV3(`P4dGLGEx<M!gQ%b)vm94km#-S9i;HS2t?`X?tRxKBL(dv>tOHnxK{C8_>L<);KayXmv1 z(}+vMNanwPr#ipKo`+ov#f*2T|KWUZZTxc5;nUX+-*)UgPh5D?y@rX1CN)Z+dyVVVV zp??vGQA6Jt68gCTr@fdR1=gI)mcKb;hQxiTJ*sjs0SBFrbSub-2`cUMe@f>LPlJt36k+X-zRdS)uUQ+VH}eE6o`iCGkhJ-lnGCy8JhC@g98Z>S;42T_uJ^^BWSmO3g%*=xauHPXC5!+y{rAs|n zS??c_%}NsAb$ExNq28)v2_8MG8PyGzY+1(hZ@RRn$wy$=KAzjy!F~J7oSpr}?`PK4 z?yqOEaJ}5z+}6y?FD4~rW}0JpqinDN`sy59p^y!rl_{JzopI@$mE z-S}jlgGG09^HqLzz06B z=gmHLde_Bk^}v#Zkx7IZchL(BaUc+2c!1g0083vl}!Ku literal 0 HcmV?d00001 diff --git a/test_input/gradle/build/tmp/jar/MANIFEST.MF b/test_input/gradle/build/tmp/jar/MANIFEST.MF new file mode 100644 index 0000000..59499bc --- /dev/null +++ b/test_input/gradle/build/tmp/jar/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/test_input/gradle/gradle/wrapper/gradle-wrapper.jar b/test_input/gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d119b07e9ddb2bd3f7001bab7611602761b422fc GIT binary patch literal 54706 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/test_input/gradle/gradlew.bat b/test_input/gradle/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/test_input/gradle/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/test_input/gradle/settings.gradle b/test_input/gradle/settings.gradle new file mode 100644 index 0000000..e198171 --- /dev/null +++ b/test_input/gradle/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'elads' + diff --git a/test_input/gradle/src/main/java/test/Hello.java b/test_input/gradle/src/main/java/test/Hello.java new file mode 100644 index 0000000..6992c47 --- /dev/null +++ b/test_input/gradle/src/main/java/test/Hello.java @@ -0,0 +1,5 @@ +package test; + +public interface Hello { + public void eat(); +} diff --git a/test_input/gradle/src/main/java/test/Hello2.java b/test_input/gradle/src/main/java/test/Hello2.java new file mode 100644 index 0000000..dda0eb8 --- /dev/null +++ b/test_input/gradle/src/main/java/test/Hello2.java @@ -0,0 +1,10 @@ +package test; + +import io.netty.handler.ssl.OpenSslEngine; + +public abstract class Hello2{ + + public void eat(){ + OpenSslEngine openSslEngine = new OpenSslEngine(2, null, null); + } +} \ No newline at end of file diff --git a/test_input/gradle/src/main/java/test/Hello3.java b/test_input/gradle/src/main/java/test/Hello3.java new file mode 100644 index 0000000..a8fb723 --- /dev/null +++ b/test_input/gradle/src/main/java/test/Hello3.java @@ -0,0 +1,8 @@ +package test; + +public class Hello3 extends Hello2 { + + public void print(){ + System.out.println("bla"); + } +} diff --git a/test_input/gradle/src/main/java/test/Main.java b/test_input/gradle/src/main/java/test/Main.java new file mode 100644 index 0000000..ffaff1d --- /dev/null +++ b/test_input/gradle/src/main/java/test/Main.java @@ -0,0 +1,9 @@ +package test; + +public class Main { + public static void main(String[] args) { + Hello3 hello3 = new Hello3(); + hello3.eat(); + hello3.print(); + } +} diff --git a/test_input/ksa/.gitignore b/test_input/ksa/.gitignore new file mode 100644 index 0000000..8bf97e3 --- /dev/null +++ b/test_input/ksa/.gitignore @@ -0,0 +1,27 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Mac +.DS_Store + +# Maven +target/ + +# Eclipse +.project +.classpath +.settings/ + +# IDEA +*.iml +.idea/ diff --git a/test_input/ksa/LICENSE b/test_input/ksa/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/test_input/ksa/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/test_input/ksa/README.md b/test_input/ksa/README.md new file mode 100644 index 0000000..f19ccb8 --- /dev/null +++ b/test_input/ksa/README.md @@ -0,0 +1,68 @@ +# 杭州凯思爱物流管理系统 + + +## 变更日志: + +### v3.8.9 +- 添加按费用项目查询脱单的功能; +- 修复已开单费用还能编辑的BUG; +- 其他细节问题修改 + +### v3.8.8 +- 添加托单类型变更的功能; +- 修复费用汇率不能按照记账月份选择的BUG; + +### v3.8.7 +- 费用录入按录入顺序排列; +- 修改操作主管对结算单的控制; +- 结算单删除功能BUG修改; + +### v3.8.6 +- 在托单编辑页面,增加箱型箱量自动刷新的功能; +- 在 *国内运输* 类型的托单页面,去除 *航线* 等不必要的数据内容; +- 在查询费用时,加入统一的排序标准; +- 新增 *托单共享* 与 *查看共享托单* 的权限,解决广州分事务所的托单查看与共享问题; + +``` +-- 增加新的权限数据 +insert into KSA_SECURITY_PERMISSION ( ID, NAME, DESCRIPTION ) values ( 'bookingnote:share:gz', '托单共享-广州', '将广州事务所的托单共享出来,供有权限的人员查看和编辑。' ); +insert into KSA_SECURITY_PERMISSION ( ID, NAME, DESCRIPTION ) values ( 'bookingnote:viewshare:gz', '托单查看-广州共享', '查看广州事务所共享出来的托单。' ); + +``` + +### v3.8.5 +- 修复版本v3.8.4托单数据过滤未过滤“退单管理”页面的问题; +- 另外加入五种业务类型:KB-捆包业务、RH-内联行、CC-仓储业务、BC-搬场业务、TL-公铁联运 + +### v3.8.4 +- 修复版本v3.8.0托单查看的bug,基本的托单查看权限只允许查看自己创建或负责销售的托单; + +### v3.8.3 +- 改进费用信息表格:默认显示【备注】列; + +### v3.8.2 +- 解决了导出`面单`中费用明细最多显示20条的限制; + +### v3.8.1 +- 更新了`结算单`中单位的**名称**和**联系地址**; + +### v3.8.0 +- 改进了托单查看的权限控制,新增了`仅查看个人业务`的权限; +- 涉及到了相应数据库的变更: + +``` +-- 增加新的权限数据 +insert into KSA_SECURITY_PERMISSION ( ID, NAME, DESCRIPTION ) values ( 'bookingnote:viewall', '托单查看-全部', '可以查看所有的业务托单,但是并没有编辑的权限。' ); +-- 更新原权限的名称和说明 +update KSA_SECURITY_PERMISSION set NAME = '托单查看-个人', DESCRIPTION = '仅可以查看个人创建的业务托单,其他业务托单无权查看。' where ID = 'bookingnote:edit:view'; + +-- 将新的权限赋予相应的角色: 操作主管、财务、财务主管、经理、系统管理员 +insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'operator-supervisor', 'bookingnote:viewall' ); +insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'accountant', 'bookingnote:viewall' ); +insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'accountant-supervisor', 'bookingnote:viewall' ); +insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'manager', 'bookingnote:viewall' ); +insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'administrator', 'bookingnote:viewall' ); +``` + +### v3.7.9 +- 更新了利润统计图的展示方式; \ No newline at end of file diff --git a/test_input/ksa/doc/image/ksa.psd b/test_input/ksa/doc/image/ksa.psd new file mode 100644 index 0000000000000000000000000000000000000000..f85eef60f1b7939695b065b467808dd335e815da GIT binary patch literal 6567131 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 z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FoB~;U}$LXo}>8b{HaX91l}-#y?fvAhwuk50TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC* z6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-dVg&Z? zeRc1v__gGAo?=*zOrYpzKVJ)|7ijXfB<9#CG2_STd|6z|l-b)Alko&&!$9sQ_k6a>s zi6Qr;pVVLRY4`pI{v1F!{_+05gnd7vA29(FFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2 zFaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-V z0TVC*6EFc2FaZ-V0TVC*6EFc2FaZ-Vfmuu7N4`wJ1m+ZhSA3a(3Ct-1vo-BtPX(a2?>YALLbhk9xm7FTJvd zf5q>Yaq(5JalOU|#(Tf~%1>T;`K2Ep=qr1TkAGON{_8n<{pg4I?=Zgh!+pQk_*6gq z(T{%k8gFv6UwwqW)1&=uw9luKv;A%Gf2tfEe6;o>Tyx`#^;c;ECh&tF#$^&FU;-v! z0w!PrM}UBH<0AmoAJPO&zywUd1WdpLOuz(8zywUd1WdpLOuz(8zywUd1WdpLOuz(8 zzywUd1WdpLOuz(8zywUd1WdpLOuz(8zywUd1WdpLOuz(8zywUd1WdpLOuz(8zywUd z1WdpLOuz(8zywUd1WdpLOuz(8zywUd1WdpLOuz(8;BX22+m~Mc_y5li{@sthk2}uz zGJ&~G;NKp>>#t{^$wU2L=O6#?AzcR-BW{+r_oY3r?%liJyXN^p6EJ}{gTSHP;P&8Z z-1F+Iul)O$Uw!4rFCW@R^;?_3ks!c(OdkGqB)+2dF=u>P?eW+3KZmCKR}T8Qq}i8V zefh^T-pYS90TX!h2pl|e&Q$bF%?^GYd}}AhGnx7CCNP@`?A^0x@2h(eZ+)472^=8; zdk^V)jW1%)|MwH&Fwdcn_ z!544M9@;;F-+EROnDJD9jlUk^w=mEd0kFbz0r(Mb*MjG4(&_xTbqCh9AN@4 z{`PPE*I#?_#jpLNulaiG3Hl?rGjdp$Wo8S1x zH}m@4Z~kswzxTVpm)Gxi*MD2rKhEpFYuA6D*B|8dKeX$A%!0NHt#5tnpWyem zzV%OW=>`7$XLV6&w2gHpZv-HQrG{=>tEINr@H>^&;In! zy6eyL`isB#^S@}wWKg|NGwm{`ddQU;3NBgzE#ie))qR{NS(f z`p}0ywCGoP9e@1sC!BENi6@?P(n%-d`tXN8!t0b%K8owK(@s16^wSqFUc6+<(xuCm zE?c&I`SKNKoU!7}Ggq!$`LU0kh3o9I&pGGgAOHBd=brnCPkiFM^Ugc}{0lBvwQAMs z)fZlP(M1=n!L|0{i!Z(e*JYPob~&zfS6s0k*OeQt+;G)ZS6zMe)f+c$x(3&^*Isws z_1E8k>&6>zy6L8yZ@%T`TW+}x*X_68K0G|UdGnSnTSs`^amThhwr$_GefuXrIeI6q zv9YloJI2SyCwT4LdDmTcO-@ZuPVJhWp5Ar$-FM%E>t0;Daou;{eV@Yh0De9A;HPmt z^w2|};q~yt4?ptoBab}#$YZ=7f9&zcAAjP>C!WOh6kmMyv!8wXY5aNyzjS@>na_Rh zbI*S6*;#byAJZ>p34!A!EGX;v6HYuK^mXD%C!Tz=GzO7LW~WeRr<`&sFGvk?gW9OK zB}=5Zr4XGYw*sQWb!O-e%7gUI##Pdz_Ncz|rM?SR^SY1%l>XMPz4#JH@X|{!EeVze zuiUU9M7Z(ljgsNDxUQ29uZIjF#G4_+n{Sm4Z=(#MLkMxpR!Q-WZQCHj?W3ckpZw$} zrNkW}MQCy7T|4iZoScFbdF|SD_pZC|ffhrE_uY5@{r6Ld4?OVT0}p=s!B0N~B~peE zB31b4qmN36Pe6uGJn`gHPeFlCJp}>cdWO=2_?~_C^SqvW?zt~~;S0}w;fr7R!WX~z zrMRBw^}-7;eEG{SeEBP1!7pB4)%9z$>Y61_R(}~ufU%JOQL&7P5fn2nLb8aB6l_YC z5%Tm1kP#yhBvY}=SMW;7wqSYoS?8Ps%_3eRT()?b5mO;EB)fJEB&(nqnq7DKIw&^d zX45Pp=f-O`ZIWcKzwSB(&Kqya*m=vX44nv`!<#ADPP6++Hlrsci{LqlYidfOMet0; z-pjz*(=37~qbGvrLk~l;kMIh~B5qQ$3Y||uu_0LoO^Ft&rDQ|3U)1#_i1zvC`J0M; z0gC0-lkBg3byi9C*DRLVLjZc^bpn4&w1~0HW0}f+SW#BlY}4#VPwh0TFuSBBwurMK z*)ucFer#ptw+OQzKj+-kEHd24aa)#4$uiEay?BOZFTY}4X1Wn&ne1+CVHUaWb=Ti; zJ!9;RH{Yy$7n;2lf^OzT_EM;9(mLb|K7W zUc7tv{Sa+t#$A*}YW#>YR*5lkW8}rq>lYbi6=a_e z#b%6UV*C{fc9w-%YjzfCRwhw|*br@oS^7nCj6VF4Q(7te(TK89%)&cL&C)+w&L~R< z>5MZaT9`<*k_B=RA8D&>@r7T1XF9~_^rI%d}KWQB@V`}zF zTpO-Lh*gl?Buh!PETSw-C1@6&5~A!aH&e0nlu*rX9)@D6SxFX|@wN!E2(uyCafaDQ zjCV@0Q^<@VSf<9bm3G5by6^sUmEb8=kj*eFPw9~;Wnn2HFQ%cSDEm~DvRyG-i7^F> ztFKw4#ndb#?N>6)e)TJot2H~DL^7Mc7G%UF+Vr1P%tm6&%s5P5MOhe7inCCxs@c=2 zSkQ`0Q{ryca4cF+f&xC{PM zMA@lSY`9O-?C$#&W@SIg38w$lidmGh(kv7!`zaFRC}!#YW+{spE74}0{UZG*6|+o? zqnd?sQOP3GLa(#Ye_~}lo1V1`{cBNftV*^lW97wa%amoTV)oQiB-!fFoSwyO_)nB9 z`!vmfqGYRC6Q1$M8D$~aOpKx0)sU=uHnNQ2KBfN@mNCO@6tmK7uaxZ+E5}$t_GTo; z$c%5h{kGeNSuOoY*;%DndQXro6JuCVCq*$!#nON3rN+G=OU=T6qGDTStSHMi zPx?;~ETb%o*=X{xnq^+h%(znHfzB)~V^y<=vK)@cC@aOXF&o9~#$IB~E)V=C`NciW zGBKwAgdp2hvt47Bnw4W*QC68T70XgKh9op0QC%Ko#_2zOMwT%rB-ojK3}H5dEXvuZ zP|k*BO#exeWsfJ@JnYLxh?Qg&V?(f#Y$V2AoSj{L9&2{iXcj?MVOExLWX24!EzGuB zmW|o4pAckK&C-3sZ;3YYVtU5SGM4?MDJE(bb4+xMCE4?vXAH%zR%VPY4{KRTR#7(Q znV14&3cQC5#P~T5Q(&5cqZHBPQJ04X6#XYA z#)`Ah$Lvdt@kPy|XYB(1QeyroM=7eyb7G{$^o&2E%$TV$lH$yZBQu6vORyTIh+Kh>D=z{nyEV`j!}{zytqG2 zp)QXGpdeah#;IB=R+({0mWpj*w(KVqvuPPivy8E^8n7Os%_(DK#;ulx0fkGNh5f|w zCq-G7vydzWyLmGcW6dl|v8-mb&4au+Gvg>_voi|=3dL-M*{WqB+510rKmDf&vzVkn zUR;@R`cKU>=KND6#-DxWnWsNjZJy7o%@dl9#;g=ufi}Wy_GQC=n#F$2PtP*O1|B3^Q5KEaYV%ODpGpG?VV078h_!4k2IK%#&Oc>lEW`L2HhOaRwK>M@ z^RUzNr7wPw0Tx%ZWtAJp&>}~mW~ciUf%d@9CBNn<5m1y>piTdY#cV`b`A?K=jTzUF zv8E{&vo)(~RttE|GR_4&##pWfL}Hx76w#W6Vi9ON1B$gQFSU6nS!xzxmX@*nC(Tk^ zeKiayM%hixGp=G*Q8waiNLHS4WyU$OxILomoh4ai#_aJRGu8x@qAdNV$cs5<9HlHZ z8?|gVOp&VrG>jj6Oe2dNfI^UEUL1=7(dKCeRCak{nxf^!lq{~cewUruSuEP4^t|+^ zIRJs<`(c(Lc3_x-vlJO+nHEQ0%+$CNV_L>JO~F|TMcG{Ekz{E}4TY97PE-5nHfW|s%EKK7*OrN32pO0 zvo*8`|A|qSnQ?Vyr!caZp0PyR2B0F!GBsA1ZI&_eVq7(`_#_Re%#5j7bY`1oG1gilkWya}0X=E|x6=Rx$#q4bD%hnH( zqx97Dr`az7MA5*{o%w&!Yvr7^k3Oxq%1ASZnTxvM6Tnq+}6gS>09Be*mnW)OhFSVg(dCh1L$r`AgX|5+jBmOTM^kXk{Z^h# z5t*?JsFZSzDm+usbBVL&0u(tb+K#xzB@fX5)q`KOi` zw{Z$hEY>sy{U;W*%8YXWikeksJWD4QyPqFN=gH~MvVQ_8T4ctEvvQzvGXNfx2B6{q zC}hT}W-~FKkr}6ARn6v{F{ded{?o@P+BljbirH?vxeE zhXHlZJ#e5fOMx((omoZM2Ojt|s@ZJwgk{WXmYvyLztjF*U$V-KIsb&#>~pn%SN>Bv zPzth~TI{dPLbJ1YpaRm}|8vQY9xVd&i#bjaQC5nL#Q?ca%8O+hOSGq|m~8`4OX4`0 z(rj$t@o0*bXEHHXl#O8uO12BLP;A&wo&S^u6o)C088^>Zij`#PKlL+XX;zX&l!a)s zHH#=aJghb#d)XMnH|t< zl(ICSdh<_J%w}dBN1SGxhyGJKP*u!!Q^o_U0kd1N*6h*J=DweYhH+K1w4kI~O&NEh zg=SCT(Wt3d6|<)+GiEUxp0OkweV#l4N}buWO0(e^L$ccEIsg3gB-!wv5M?janQiLK zYW^vT*@&{rj4`uVnQ=5`E6P%_OpSB?NjtM_%qq%m)-;7QJ5bC*u^6V%oN=3`n4)OA zJ`WqSJ;_3`VHqRJmSr57F%n})R@*$BGmh45SjO~>pQU7bj&VzjQ?k!jYnC2VSIa`N zvs|?MK9~H+(IKF`I7Sv_K%H7dFtY!P1*_c@@|EV?u zT4Ky%w)`gzQ?QujIg7N6SzrpVT8O;hwT%j;3Ig)oK7Vo0|d9tUTk3 zFNXaD&2pFmlGQ8)!z^|KuI4laf-J)9wbyc*BDy>bvz=#LT^?G-@}C%HX+LcnEdy$7 zEDWe|oIak#tj=tkW-(g_K%ttw4>ODR=g?v+W}9RgW4n`4QO#zURg;G^ixh30KQ7NW zR%T}`&4y*n#JE-iVw-37OV)9AUo{=2N1$0KRz04upP<{&EG*-bBQuU$^T2_MWge8W z=<_fyrd~C(sA5){jaoLUSr|~ri&e^YmhmUf?=s_zvl(So%fd6hEJVvGW7$uVECkD? zS?u2_$TG}Qvxu`dvCqSq#ar2$tthLZ#SF8QY-tuj7Q+V|95LW0e@wGp1;vSg4j+G5(98#R#&{D<#Xt02xp^f1GXE zT$zn7Pi)QN2=a5_Kd~(v`*%1Q6`nDIEZRJbu^gsgh;4WIP?*JW#$BunMzGjJfBoC3o)3 zhGWdeEbpd;S&DWth3uzrj1^`zu~?<7>?fX0LCLC=RhvhBo~UIxv6y`x%|9u^^89gC zw6oH8>VHvl@7X>-zG7*Xelc=mYL{PdK!VhsiL?a%c7e-2jiX6w>TkEe6#6?=FjtS@dNGCEJZGW@Glo z8x>~5G42^qbd1rMMJ*edapc8aG0QH`_)eTZPX8%2%TjiK#VliN_)n1-(|^JNPzdaHf{v^d)=;}or? zj5ExJ|3t~woq3oT(=k?JtTs=IHUt}zrDx34`0QAijm9j(EIYHS@Tmy|7i*NFBn$ruM^iA&Zg2LJnmmZJGN70k z&zPmS=bqiWbN(sPVp+z>i{qY^10#!ZJY|tFmiChjs8lQ>twbxwIG1MGnZ;&6G-g%JA~WVP4>kjq zFVpl>o{h@+C$)LvFk_^~aWkzjpcG?Q;X*Nc5#uaV;|Q~<*~>0NVhqiOWMh~DrK~i2 zO*+OkvdCJNjak&PC}lPO#3oN@7BM#ZvPg_^!)IB>yw3*~@SxbLWjRZM%(&IEN{ltL z7+s!-v#}V^YxA(l!&wTJvRc2xnQhVK>Df;lT8vu;wD~7ivbdhd#X5FiWtP?KoVgaV zcHW}zoqjQ{0m*W0mZ>qOj3L_YXjB@;471@rovtt&wX8JD+&E?_be?gQvR!AE<4>{X zjyS7YmXc*Lt7;aSmH)(AmUresYD~`>~bCx2iS%z8NOpB##S;laT zCE7@gA=$c{R?R;tGiI2jV%g+jmq!OvM3;xPtn4ScPZBLl+33tNFP3CE0LAI2kSsg1 zFpRsES@wCV&4WP8zAO@BWyaEMj8bG@R&jQ&i1sad-Y(!z3(2K;bqY z(ClE84b5gP%e*-KCkPf-Of2SMGAL&=%tm82qAY`~{3qUJHZ5aGwrLiUt%E7FGTS>D zwMG^>NwJS)V*{@nrl9+z#5flN=o)Lyy@IU#C*{R4u{Z~|+y{S}EMW1xW#4*+StiEW znU!J@XS-rnd2!lL6=qq>;tI{Gm|a237~@a8X=O;Z=AU3eX)i!w_PlKJFfnGFT}}T9 zsj)I+G-hKn0DW1d#)`6(Y)08u%xai|r7UaNHf9`|aTc?(pA==$P#3fLZ*SQXcj10wNmiw-6dRT?GUKLM4L`MG7BkMChP+tCta`JkWjSZe zHcv@b>vx)2r2Q0@G4f&^Cc{w*rN*nNSS7|G*$A@{Wu;jRKvpkm~*W9Bo%TWp`w&tHwvN5tq&o~m} zIDni}#t~(?fX6B0sAl_jhOW$*$1EZ<#_{7BWtAC+YIFat4H>I7JC`KunTn(RvqJ_{ z)v~QIi#zk2e3H&Z)%cU{8NkF?nQ^AZ3bKr{Ic2<@H*=3T%O(#JW8BSMm-;+x&6Z`% z-Yg7bUMgj2KQYY4Fa>K_TAP`rz|qgYRH)O6M`%^1G+F< zomrN%OpGbmsAVD9HnaGAw0X46(;Q4n4WQE#x-Y*J44?_|EaT#X+OozEF5D=R+%xYS=O>UaW8kp zS@=(!rC?@!FB~XIwvJhBlC1-v(3$1EdE`GK%IbMG-lhD`!rKy%~BvWMwCU6)sbyEve;$DI)5B9i;yh4JlW=n zFw4Za>?hqapf~^2Tjrr;QOGL9DmU&Lv#Hs+6ljn3Gj?HrzI2Rx#jH%@X!FQ{>NjR1 zH%6a_^H0i*nHQ@y8^vsBR(Y}7JZ=9j*Y7gSwl)tG3;U@oV@kHdEXFA`vk3o*n&oXi zBFM%t1r`Hh%s6i|tIlk!%$8=i@PcL|$l}PhX!OW_ip)4PTWuai*{Wv4GiGb{Q!O*j zCXYO0WyW1GtEok{dBQW!E)R=Y3_vL{*34quu`=h3l^FL7sPv4Z$>ZF3Ve9fB-+i4+ zp)kuhtGu|S#&nI97jymzGm8wd>hr|RB2}wlidg2UF=I|Zsg|wrCmq_xD2q5t$C#!u z=AW1vL$qyX@#46f7HV0B*~p9~*t`K=JkWRc1UE zo^cklCE2OEGf#NNh_W$f{HgnSn~$7W>>Vc445%7_Qe!q|7IBvkUXm=kJW%a(%#4v3 zw>A%hEUvaPn``bgjA!*d1NN7rALJS4SH0N;dSO;W#!QS`X3SZNE;o)C8=B?H?CA)! zOpMEb;-K*vXD~NrVw|2aTeGr^&p9U~`-$w#p3jj*DHawK?Wgdc;xHK+P@!2)Q?y}< z-pt}PQOl-gH2@{ah5?0YHZ?mEE3@5dKt$QGTys}v7S$||reH0rQkI%kW~|kKOpK{o z#93_y)UA1#8uQWwRLoLjoRwxjm;O`k@=&repm2|mC}vBtkS#Ogh_j*DIWlKl`hByX zmi~ALBal;z%8eOhL$U1gbcr$DCtMKiV3g%uJ``qYKtZv+YBpN4VHu}qtYHd>D78Gw8kY~v-%$_69IJ-RZpQ_7KnpJ0(rEJ|zE21oxX5~LYvydz@ zW1WoJ8-Th2_nFOQ9`4`iFk{Ubr)7*N%gneNrl?s8c4pIniU}x1S&bRrqbNIIK()*` z&upV)nHfK(vt*iQ%or=nn9~%>jCGVuw0WYOZC##y3@9l!=bz?WYV7{(n=H$Nez??Z zjz48)eDXk;4aG9bW|)O!IZQ#x<}CvdWI0XIOyiZh%?I=1$c$@g_FM(o?96hQqFS?S zkr~4@&Mr?T##-h69$zDRP=Z^G_Tz&eZ@GvrLXTO98=3v#Mq1mSY?h?VI|f^and2fzYc` zW9pTeaqb1Em_7Mq?eeIQRgkR#sI;HBGb_bnohOUg?93v{Mrs@ri<((vZ&vmbgKVV6 z5oNW2hajsc%PC{{PqLqSi81qHUfJb|TK0NrR*l)1SyVBra~2t9IcD4>%W9UEF;ZhH zR;vLOX1O&x9mQ;I^YESl@SpCFBim}2f-AEzWvs-w)2w!RxXi=EI96tr8FvFvX+PC% zW@BQpvy7=(h1qEH%(*D*o%aKZ?9F`z#2V{7jIu4rB0G*+R`X9ewHW?Wml@MBmS@b= zn3APtGc#r>o064h9JQ>*Db72ui?fQdkr`vnovAVGC#J@5j0ZK#)@+D2Hv^hvQOx4b zJi0Y>MOjLAGiMgnn5|)oY|KV63(fY%jAcMgO`*+`^H18plVo*hn+z!WPjQ=%*qP$z*)Jf>V#^&1c)S#1nHw|4mS^0ijOSWnj8OaLJ~jQ(4oD#9 zj8n0Qvl(V#8RtGvj4ZN}?Z%Ab{3l9wDdTM4fTI7TE>9Xz>hdtma?Ds^mYK0O16HA$ zg=G8F6iu?w?7E1uZT?A77Mev~e2p~AVivV59b=6w!ZJpjjSakN^R&in%^Bm!Hf;v< zZh)t=WOy=a%uYGUN1&t=R)UWf%G@7-!pho#%%20r2)0^n(~af8W20PkZc6m z*bLAD9%|WG=D}jX9rT}2%*HxTX2ugrjPJS&)odLn!@O9ERcn^(JSt{U%|7_R18B@r zvKoM5VoU={p0V5~YBoG$ZU*oKDDB^AYc}+%Q3`Z*%$da;f0AaSm}Q%X)husW8TJ!GY$nEYO|9|uE_gK^9RN~n=Els7 zai#sFVwO>MpDam&h8t5D0Lnyp(_Y5}h@V}x0aEUwSi>{VB7fc+%JUV~!xTHfX(hAB|ZGR)@D zsI;Gkhw(exJRE@H_*0B5Mw92x=<`fq04nYoK>w-D86z{MV$tQnEdyv7qcMwL95PmC zwoNS3Gmh45L|Ig`S7 zwwjL4cTf8XSI#UdInGkH*-s3!474*O8#fJ5XEvvdSFFHlKt@>}jmp%R!xYSm@n4=l z-qUQ$jMwnuGEdKcO3OGk%QjDhS=|4Q_vTTQz5aUDve4|!FraSL$f7h0#csx3W)Wv` z{x}ue=AY`AMFm;hG9V`wr*PG7fCL+ojn*t3V@6qMmi|+nOc67SnlgSohA9wZE6lQ( zP0yH`tzx!r4GqnPXu~$9{{+dl#CWbs)^Ya8%QghdCQob3*7y@0V+>Q^H$3ByoO&uW zTZ;j$%fmho1dGFrkr`96TIPu}+j7laQ^vV7i&_>JyFBMtknO^3f6ZM@9xTn~_)~bs zSK|OE&Oe1@b(+i#VHvB*6L;oeU-tIfsaQJ3(kup`WEqG5)aIX*7~`e^llV0?HHl(2 z+B~h84a=B`u_mA(*>sG1{u3skVqh_XECcP+I7IiEgVw4EXOGrWwpyg$)d}{`KM`a2HbsjNwy{y znHk5_q(#Eda(SxFY2aqRNcVnABP3bUVxnZ^EM z0Ea0MWI0N)7Lv^kJZcsbi=kNzQ*dJz_EUMr(d3E57$b`}-k8G_jI+>e%u-OZTVlu< zi7{g=t~O3M3bNCzW-q>s*Q&f0F<7XcE|^q)BV)cQPJ42WYE=X5peUeA$MEj(i!v&hUiBpX4NQC49Vw}zJg zM9C^K<}3wsV@6pGQ^b^U_)k&FLbNM!(J_w20Gh_otY#L|f8sHVYVuIBO|x>0FNrX# zTL#pvp{3bOj3df2GtQGK5M|SVl4di?W|+;BDQcL4iLq{`CC%=fm}s6c{U>P_qQ$|e z%8PSiQC%Jy#--SS#8{p&$Bda7$0$X#Wn*YDqAdKUE;H^g;7PM8WIM|krR6^`#QDs`WdL zKk-+XPjlNcc7OKB{y+)$iHUI?CX+!{wJa^;FpVQGF87H+Hp6TvRt8j*vNE7D%F=$S z&43nVTQSS2#nLRptQ4!(?4_3~&gx`Tma~JkY^T{Ara)rcidhP_BwL=b{HIKfKdFt` zxM!t?7DKZdGY-j8v^laEU7k>EX2x6#$emf#vMOeo8S^$D5oVPcM`E1SY#LCSrC^t* z?lb%R^U93Ve##~f<;s&}=C%Xn-1tb!7M?K$TjeYR?MWw|tWq{DW2DA${uBJC+H$YF zIPIsDY_~DnPFW1io}(z6>vyz_ab{aDGj1nSsFsyr(dglQW;JA-Z64LK^q<(~(fE_1 zZ0pSGHnR$|o&TiUd_b}>Oo39C#Vp;YG3LcFPQf7CB&#^f#F)h_%UO&pw*5P1#$9U6 z;U|SzX*MLQ0}B?RN>O8==kvm9CM*6-jxDai6Z zAEDSb{v^j(<4*{)4`Tp|K^CIbo_p9&)tPOQ)jb2)nMI(jiN!dWBA0o-#2yc_V+~U< zHE#F6bDTZGPy4z2;X|@ajB%Dstn;XvMQU7WakP2*#jNh;-q}xZjh9QZJk7X97AaZz zPvIHMeyTP1h_X;DZl<*w!xXA!^EMxsw38_mW~0j!_YBC9MaJ2tSwz_wGuF%^OW8R7 z30v-w8AG#(v)iN1!^AklY~M3hl+`eWx;znO@8y&+hm4~$t2tv0Q?&iNY|JvuR%Q$j zDkS?%n#Lj7Z1k{}?H2Hq88^ko`Qs605vb;RYV6s?BllB5noY;J#aMKCvYeG=oTBY{ z#;2bK`w3T9%r0NHth1j~&1#k+{HM4zbf;LI|DK0tAWmU>@mxr2VUd$+4nq`#5Ma|~^9S^3^FhzRCN{x8~JZ=WWO)KdbGc(RM zPo4jy%owS0+_bU`D7AUgfI=x7E3-Uiu{>jh*{EeT1`*%733IkBN%p=Kam;!6=8D%Nh<``#c48V@1=07nprf2+Q=Ec~)%fn=%md$GR zx#y4=S85E!@?vMUQ*8^gb3Mr7>p$qz&aXUN0@3F|ElUGRwQQ^gD8zO%i@AP>)VOXM zzzL}6%xY&A)hsWK8JA?!fnu2LrYX*gFpDU=iWlrBOd0FeJSka*S&CKzP#I-8v4|Vs zAvK0#+b&P%KOx9Qn2k8A#%w5dYx+;nEXItP7svix^Ne9XDKpl=6xo?on}?n;YFQ3I zA;>DshGWbHJYL+GrTx^OS?txa?93`Pu0Wd;ixp*A%2Kfkvl49tT9vbS!q5HG*qS|D zRldgiL9|kA(`@9$C!LhzPt1+w7;~6{g5~*75-e+3RI?#jjV$gf+1BR4&9rou3>{;n z#yree#VptFA~o)L#*!?LY>Syio=m}7w)sz+Xg}5dT^puAVl2ttHr%Euw&IG!m~mE; zRbGtD7$;Mx&7;;V4Jgh(g=d^2i?W||FvSBoW&EHHGnQqnYBoIM$Fa?mQC0&`xy!>X z_wH`)T$$w}Pt~$?jZ3l=Y#u*850++o`N3;E_5A9?B*4sAK~^)y?9A2#)Szcv^G`8m z%-$^S%)?4HM7s>4T@fc!bYsSxf657{^o&)^qMBtfi&|FG6fpo5(-aD_2(y=`|Fj;( z?D|{`fNGf-b2Z>vidIb?DE7wa^01r@%NX^nYFP?4idjZk9N89Q##zheFvVSQH}|R3 zY#30P7~iib%lRj4xz_+x98D3WY-{thHFtQ%wHQzal$K^&F*|T%+l<6`KIO%f?qNP9 z{cZ;(5Vb5+8$-qvEPsdll;ckdvrV$R&uqonaGzq$otkZ9#xS7NxShm_?AyWuBH8hhlL6l(u;gWue&U%&IRNnK2ZLyf_Ap zp;@R_T^`y`8i0y}Df*Ui-I)jeQhS~LTG6jm+(kxwLiB?^n z%#2gAF=iY)vo*6=k}b{d=-ND4%ueCTI2($k|5TFAVpj7{bd2d4XJ$;z<|suTjhe$0 z&uCyV&8Kbv3RA{k=(;@NKZRs7&>}fTk7pjZPmvuQ?Ahnn9UcK?#+ew0XN<(S8#Bfn zg)Rz~zvVwM&ML~Pn3Vy=T2^6JhH->hNH%YPr!J2gvpRu1c4otWYKd_SQ$(2UcV=;C z=!~)uEM^v~H5+l3!xRXz-D<$Cx8Hu7JY!|X%`)!JY*UzJlZXBj1k2P|{*x4|)L4C4 zbY>$l4%1k*Y~{sj^E{Gmo*aK-l%-{iMh~mmvWz*fn2lLEQ0?@mu#6RF2V1jc8qej7 zxck} za{rE*arsXjI=lmsPLbfWvtZz7PH*uiD3#DP;j6$ zOaaR{idp2waE!-7vblkW%$Pwot65GNHv@{DSyrnW*=X}H zFP3H{*;wYunZ;UkZ?!D9dD!F8Rl^iApxSOg7pLa>{~gBD((iIG0*bO~&0_vZn$@j& z7-ca_5!Gzm%zYp+rfG~Qt1yeqxOV~+Vk}1%2WA$fS>6BcJo!(GvMOb1Kw&cgVKzlu z#cbG5I2o1YYz$MdmJI_c+dOqJ1vBHD+2pA;_uI40L(L+}@&xi2GuD)Gba^zh*p_)1 zW~X+QY24j1Knr+0f1F`9x9?EOW-*(c*|r!U)0mQFZrm?s6=$_ItCd+*v#D1GTFoqK z)4fI6xh~Y+*MG35pI>*#1f*ET*yHo?CoKnHFF=hR4M4G$?God*fftr>ws~S{wr3ej zv266nGR9&+o+fi{Xf|&QjX0Z@aclF$0v^YoY5df~1 zCq-E*mYUth&Mf;pW6~@$3297 zfsC^FLqYb0X!5{+VsEyaS(Ikeeu_3vi?WQd8D^OmhyMh-I2%2hSnM>*YL<%u=X1!I zivc{$m{Ass0W^&HL$oxEq1i{$enN5_nx+4w zG2@7`p9{%Sw4Z-A?-&qwnJvvS&Z^B5{*z9VQJC$8sCn_f2YUkgb%#zsYNcXx{z(Bg zhAAN0II%4i3-{?%*u@&AplPhcI2~hn#w+sp@iWd?$-B%V$kKkoT|RP_0=M~){Uph1 zVv)5hR|A+9<5x9#F#mKJ?=pJ@M;2)rYyXaUG1uG?V-;n)VG5o8gv=PbJY462|Fjtn zlx8VLGBIXqj4&IW**hg!MA^JE&mdYCen@5B#h-^k=2t zasVnMTX&gVp8iv=xkIx}vS+I^ixZ$= zKuNROa#xs@|CHyC*Z$q5P%QIe9*qjgre<^gNyV%*OaBRR_C|(TX;!r?6-&vcW}#SF z#!_tfPqK{Fn2pRh-6se(JG07+Ay{cP9Agx-EM*mBwacSoRgpxNxqa?8ETjI%QvQI>5UtpO;98^1A?_TM`dtHpqDjJrGY)HV<8C;U2Ig4 zZ>W*Q*uR5fb)8mhkZg!nivdiGHUG3^32#|R%|f+#{1d}$ z7iF7dD>0tY=DDafdBT6f_)|1yV`P!VEG5f08{<#CD0{;VxS2cMr<$dx&4A6UX1iq` z7PHxyjW!Q0W9|ji!4y+dDrVaP#>Wf{wVVqP4@ zEKAvpvPzBNKSh**JSX2u$TLNQCtQnE^nd74bE1}MzpU*7g){t?YOQA3u<4*{(_!ZTx0&RH4+2(1bY_p7Gn}_pHwHh!`3(kkX^qzJ8 z-#1Etm26kd#?CC_EDd8>##(cyVxyQFf)c?F#xqiQC1_15G;3Q z2f{3aEXRyH#g_kcH*cm@ij6K0CCj|HJY$C0w2VWvkLz4iGy7`M#$+RlHXW!xIG zC}$yAYBu-W%QB9{SgG+CE^Y=uu_0NMvq+1X7}qpK4jF4TAnd39095A1%8X-ZF+F3R z|D^pp4pYSZ6Pr9a{nWcNPcxuiV4ziFww7jNFJQjR829sodd~UJH%Or6#jIuF8u#z> zk(n{?<(_?>zGckJI1DJ3vIw&gWMj-&TkbTV;2CR`NBeg?vkjhcYz8PXmSxOhmYy*e z@DO7|vD9p@n5|P5l^I_fmN8Oe)UqjA%`CFV!wYSm&G4V1nq^*`lGQB5Sf<8MY^26< zm~li|t;~jGYnw-waW}L05RM;bjNR9N;+A_a%3^2s+2$FCX&h~y<{6`ujn-_aw(?>K z)|!2T&+B>o?h$8M%kn~mRc6c>o8wQkpVEMWU=?P0U!Evs(=(=Ed4*%lE)Sz@GoV7W zGnaXqWaFNd46~Yl3e8@|^PjHh-T#i7jm#KB#vEB>F$={qGoE?OVg^}G8SDOcbucQc zS?=@SA*0Nd6sx`~;r#u<8ml;2S0Vw)Uy*Vj zX0iMyXcqocw0W2q-z)v+7{}fSHiLp}SNQ_m= zYGqc38Ot(;`=rcRlI3Cmido&Wl3{i|MO&Jc|CB?EvW#^%cf?p!vrw!CpdvA5F}r26 z&VSmhrM4qr{ksE&J(o_wSG!hh()IfX*zZDK!5SiE&O-#Fl#wK$T|K zAj(E&%+@TcS#@TW8dsdXivH8p2(oDNz<%N|MM^fS+0-mY7CC23%UH#1Y`OC^S#@TcW;tZ61E821bD2kL?y-LtE3=5Q=*&v7C0RCkBFg4656juejJeE{iLnf* z7G>o?RbpJ{KQ#lY{HGl_%{WYBWyWDZ<-{V+S-ckucp+IuS?=G3|AbCYp8u4!EQTpK zvWPItQufIw<1Cq2nWbp+$TsE1*_Y*F0De8sHcuwT=^9h7+P`z0ov(U(bG|RdSQWDa zvlP9|xEWB4vYIniV>UcvO;apYltrA?{FC}TWkBVaaVi#`F(s=3DBT$vyF5*@u%9Bz zhH18gPW}}*AIopOQIAol+ z=7D6D7)M_=O4;r-nSqRQ{`}?5dCo5AkAiZxWyZbL0L57jQ!vPuWKW@G%=Nog%(gLO zXtpz;1`ei(Hjg^9ZJCFn?HaR9vTHeJoSAXgnT2FIvdCcyX*M%s=Ecm6VHmSBdsF1a zIZMGXt7(dnt@4c7<&kE2n-3PVJF+zk#iE*p{e-JUSxUC$#f-BEvW&7Z0o8?BDmMHl zgjqVqu%FQ8sl|Y zvlmyJr)pUS*(0`WW)Wm%Krzau`$Wwu zGj4}0PSJnDFhv?rnt)Q6{hD zW*KHx%eE-X2`E{{YRodqW@?bDBETh8D;y4G3_T6vs}Nsh|4@N|5W~y5@QZiq+~T^j9M097CW=h>~+`b zWC~4FAjnd(x852DKw)H21B+_&Aj-1Ovwa(G^C8LhGGq36y3CkimKVxdYBu^j%!?z; zR%Wb|QF)&a%|9{9a?8DC#&wwSGq^ggDFC@s$|jSL1J889(HEKfXZ4HBT$rV%q%i9&iSWo&B8HeYRpwT#BtI`+1z%7M?L{*=o!xG44F$GN9&tVrjDQ zwRz}2VF8a96syGmXqHhn{3o?}vNaq2Qz(}HQ`EA&XQhH{TE^=0q++{j7VZAdp_R&WrSXGjPJjx{cEv0@vr;TGWBE_4W}(lh1uty$7VqFd3w`N^LLV>tIyxqlXgMBHw2rttOlUa=F!S*ugQbcWKPlf zPq3iq7{@dPhbc5m5gT}1zgrfXg=lrp%Dl~N%q(IxAZ|0OGuyNp5Q(t{pkmCpMiwK= z_B9(}7M)o(W))@8hiFd-J%1aA~P<_nAPl^95QB5JY(g>=^1kXYLnuuW+~`D@i3X2saY1YIDQ>6a?HN#Q07{ZoIV<-GsWHMV@?xgORBW_) zwB{cE6QV3e79%%K|4A`cdjXhPj6M&>DZ+q?38>WUycBBhn|fnU+lBnzA=%dDfdLh> z6a({5)tEiyqYy3cf5*P;;>D*UF=l40tAeZ)o6{7r8IS`|=O{78Z9Z^SF}rG2Q>-*g z|0$zvB*t2Er(_Xh6=o4-IkFhT6!f3q7~gnH(SRDoWWD_+vavrjDjyW*m;Ojz*Co3W0@yJD+3Cey%iUNtm3SODHLUM01C@I)NGd-v&X|O&(3J`g#DD6u{3+%eY<;O z#t$&cW+_{qai`ggu{=$N`*%-26Q=Rt!4#-wW6GFmaep;HesRt$DlzWGqxt&3hyR>i z;P0t0dqU1ER%doVvbbwN9A+Gjv7)Tj?_x2acmC6g70b_PlGX7~TEII?j&X<ggd2nlrDPdonHeL(^0!V#rT@g#SSzy`Wn;>i)hr9zm{^2L=X)`WZ*hSu?{L5UNQ^Vc zhGHW#mS>DG%Sj4cy#Xj$#C)xtKCw5O6N@TmRmy5-mYH#Exkp~CFw4Xk zah9I36w4qR!xSyb_S`3pEasds_wTMlmj{YPF`J_l2(qYUL$l0`8D+5Mjy1EAPQ zjO9N?EepxAmZfIV#904mq-s9E_>JhQDQS&B9j8#KxiibWc(rO-b!OL4v#ez=;mH)bs>=h-=4JqcEd3{i z*_x%eF%C0M`w5yY`za->%>XU)Xq%@JV<=YBPsogE8AG!Ov~rAVnBw01as@9(DQcNV zO`dMZI2_|9S^5Wc?$B*>IpD%px(4)EJd4E+xhYvs5f3 zi&|Ea)zPRWS?P;*vHKu60`KMZ$MTdu1 zXqKjNZv1}My`&4bKXiLq`o8>ul3V>-qIQI?|Z%|FG+B0Xa&wiji)DdWc;eUxD~ z#-D1KqHgoiW+^!S!~v-Oz+&1@U6iH$MAgPMHcX;2t^5S2OU7iy&F;=4| z<1D8ZtC(e!WojI2?rP08$@11brP%1qA~ELi;|jEAM`j$JF@vl!<5jWe9-2jBtZJ5R zo+@S|GsZ0gI?1O0bPcYujFA|(VwRq9gjpF-(U`3vV};qWjO9RuXM7jptPO4Aq-Rza4Daf`B1&0_wEjUL8XN|s@^#-G~GY#V>d-GG5&wnf=<(U^_Q zxXyo4W-R|nl4V}3FpD6|HV+eH-2jiRSw-0xSyY_m`A^qNvJh=6W|N}k8AAuH`F;@dJ%yMRtL6*sJ z^NcCll&oec*qB9Uma`OC=E?ittw;kZmS%A`Eyh@kQYgyi08~qj)s|hgib0l{apxIJ zv&@U3*(hc?W}KOEj6dlBa)sF&ZosXf(d6NfF+{5%n+DXD&6OEPo2Q*76Pa<-EEJnF zi*p0f-5eWH}rG)2=aqU?UES!h-#qiUCjffkC5@h9cQ)#gdTMwo?SQ?oHlAqPtS zQ<}zX^H8(QjIqfR{uAPCSImY1wLwu9iE%i_tuu>Z3TSpln}?m*+L=W$tJD~Z%`iJ4 zSrxN%jMx!gfy@}K zS==)qGUJ$kic=Q5TLzqiVwUT7@Qk5ZS;lc>8@fDAvhbg(%|ppD$TBfzYK%S)#EdCp=c#%*XZa^qN;%{ZHmapyllu#p)rq&AO4d&tk(h5imv&E}GOZp{4wKPsJ~(HbQFbJzj9Jabm~m+K|7Y)A z+fZC8R6y_{Dkv%l2ogd<0tQGzfS}=)K){4>33o)Oq9Qlts`YNQ-dcN3wLSK< z+WXO3Tia^ApHsCLZLJF0=joALHfMasnDhO9e=B<>J0Tmh_L}ehW$(SNd##z@SYysH z$DFS_vrGn*{uIyR5tfB#v$8G4tY_RU8OH(TjcqPwofk(kZv9EQarumqjN7u#vp3xEa+@hq&EkLB3{W6eCuWM*-4^{R)vT8+ie;%~H^{DHb}7b6jK4{f0V!rv z%Vq#in5|)+<$p?_r}k1%&MGSwWcT}@oEWdsfFm-^1^wcEd&-{n_io8Jh!tv+6<2fC zG`sH75@s*DNPSt+Y`13J=J}$?Pq`XeiLqGrv3dxzw0ZD9S%8Yq*okqL8DEV46v*03 zQG@Q6+h%M5%JLI5`Gy4$> zP~ll-W^0`cnAIrHDH5Rc)9AFL@IHP2r9VB4Vpb$;o;7MEKy8_Etwwd9XXk$sVCnNz zIUADAFpsZ>F3djO@{=Y5Vt^{mIHLg|w!1u?7#p&YjM0p#WpNoV0m_KQw3ur?kc?mW zHL)3YEo;acvjS|pjH}P%V)oUyz_PdAS~v3;vqEfG_Dyeki*Dwjm^I6i8A~&^&6vbE zH$zj+nr3}9PnH?in7h|M;eYY~p4vQZS+r+y6OAoDnP=^#*sYVfpgHwy-`stA&dxhS zw7pCwxiOfHnSx@L@w+xa)iPuIj30#nMP6K~@yd(cn-$4Me@bUojoH|YpZuhqXbj07 z&}^o8(wANSCthI zo-URho;~*;LvcSDv^C5F&-$d4HDnJETV4uH^8~W0X1UBg7vPm)5y@DY@p2hgVw`Cn z>rV#*cv?Wtq07^l70WLFlWfK|K)IG>n8%|5H5t&DrI-b=zDWGrq-b zo~&$JqXD<6&GSa3#n>rGjHMdyNH&m_XR%$zHGoHE>}|%~<*{WE$v7@!+l-wUBR{1v z>p6EN$I6WFR?J#~I!#`R=G=3C&dz&+*LW#1fY+E6WMeWG(>7+^mknkMvMy%9Ec#Pj z84xc8Gz(_an5D~8G>c;FLHCO;9n2K&%UXa^G0W9FS6^MXxnFzj^SI{2 z#cV^iZN?f5z_a)=AWLezt7YlT+GyNuo^)m{84Iw2Y$e8KS@b7^7Me{lYnEN5tP|sR zqd!T2s?mTr7FU;tTGl>eJWxH%dKakaT!z0(&brO77?Im^2D@QG#kV&nniyS&C;0N5F0mzs##&SyF9gkTs&(t zg*cXjVl4ehnEiV3>^r@hqA*KhtZKF(%V2=6nPmbm`jaK&Wiu`TYE9t90j1RVV{~Ti zero+md9ekk(*o7?eV*$x_SC;$k!m#3}_coGYsAX>33G`j>SlWYd?Vy4)2We+MqS%mq?vpjqK3B+y};FV<& z0qUaM4DIOuS+;mKi&2|p z(TtrKYcp!i-${ScG>-u*$iDRuvovOnSzq!Y%x)}O#q4|DLuPz18L(DUbS)bJsvJe@^pld(i&H)gj1JYhC7v#l75Wi1-}W}XAfBA8Vz%QdsG zY(qAgu>`2P8Cv2~-3(Z^$>60}c8Y4u z!n0l_69*Iml!sOnRkM$K++)8S{Rz>yF>8{IW-P?+FbiZa zeY$wIUB-yU&%9Cs6jlm1c~r}S+VVfe0QLMzj9&<1>GQmpdp=5iqMCJPOfid-!Y*Sa z#z@AlW@UgX&*DyglK!O3SS(AM=j{kkzDx`K$w$TPH|P$1(5Yd(CNmM{y=B0m*k+q0<7EM1-;7CS}lS>$G(B|oXnqs&-~DX3-N z!kR_ory30)F=m^LF6*aXaC?r=+Ee^)0<6A!C^nDiU7n9sV^&#l+B~OcYukob{&tGNc#hw?AnV*X zpjD4Yda*(K2wN7N8OKYZWyS*Sa#Pe~fQngJ7Gdm{1$aq}Eg73;gW1-f$c(uFul-Mj zS(mcVtT3zbyB45~S-L##%;uKajNdUd`&U)Vq8e`kR6*7gcywk9vmOk{HkrE29hOxw zivdcNtP|r%#;~mWJet0vmOX6~cpGld@riq?-%U)rdOSTFV9{6#6ph)GvjXe}S>?vN zVV-3({$lTJYm$x0xM^0IaW24vW@|IWQe*aW{DcT3x2a z#n9z{QZ>t90Ik_28HZ%;rD&hAFw13Hr9Lgw;xa&OGqZRW-JHe$^H$y8g_B78UqCbIGAgf|lnsGr^i7}2v@odfCeMx&3V}Am)k)I+!X#o`b zpP*T_WxtY27SQ@)9^a*vytr5vm+^*KyPtyD@;}j-^}QndU;U$&RY zG-#6;~a?dg7>AQsTJVoYXClSlJ+OEU(q`p=$4Iz2ck3bL4=x-pwt z*6W|#16E(Q{ZBO+pjsA}F`02D1FRXFWy}3^$|hzHe=ePFJf7QkAG`{(Wu>tH z$(VIsOls`3*fd+MSpcj5HkYy86cUV8&U*h-{7>u700gML|H&*Xo;7AKxkQR_GGjy5 znz32dE8EiML4HCpZv6?C1+)gNAgfg}*9)^RF3aMwKi!y-*))1o%sMl^Ws{$lnL_&0 z-u?v3#sNiQTuurgi_MtK7?xdje?7Wy__M>jhHfTe# zUL{ka@!=Es6uoqA&)QS_UYHi?@j$Xw%9>@HXysT8WStminK6Ef%mp|#c3<|3B^kGw zqLwUTrhsOX7DqGAW>gikuotHp-J}o+QRK7$DT{>`$s=r9GuHdy0!${cJk5XgtSnT$yp2JOx><4A}1< z?`l?+td2F~n&lxgrZIbv7>8!%rO2AaZp^wh`^55609niLq!liE+l< zeVIF*Sve{6sLa?bE0T3)oW!{8PhQ!kYi8A%EyM=1xf$9svr(U%8bh-It*@DtYD^)^ zIYpd$wr}+upSq{?-INvkm}SGW7K{tEB^kRnTZm0+tZFuh4Q6S~*3hgy#$wuN#$s6; zpl~cIHD2;l(JZyBP7>p0*|?0|<+<|8@T~PGGGqInjM(QrPie8LSu$f=7CSMn#T04G zwwGelpT71MX~vxyulc*%sAk{zM&-t4*@CQDRy13SDYk9K-IzsyvXvsYY1R1M%8Zj3 zQ_G5ENsYhV{->iXn-RR-ZSI5FQwqm({I=cZ5!K2_(d8_&JeG`=73=ZHRI`ta{1lt< z!YnL{0QDFhidpfj0W1I0W~Nwfifl&BV8F%HvLJTpPsxmdESa%bw)T(rEYE_hUB)j^ zIcxn%iE*!4>{{0TCv|yl6lVKo9{f)(W^YcDr@B04fO>=EC+Edr7BhwNVovhnu4d`X zrkFL&iexPrH_gUPL2Fhl>sr>uY(W;pW|_>P{v^G4Yx9_8Pdm|`<5TyPzFRcoeJvZ! z*s^i6to5g+SzN|l&C=z0wAws{S)n%ilStOZENz}DXAf7lc`|@Dj}e=}fJL)HtT1cH zie^*G;(sdrDVZ?>)O9b&1$g+MWHR20+453oF@^P~_>A3|1+-$>5}>LxTQ@^Pvd}D+ z#lox&P|)mQVysfuW{QGrY{n9e<)zRA$+$bScou;yU7qDx6wfLr)_)%>{V8sWk83Po zt7p%aGoF^$ol-EK%ePHdOlMa0EJT|lj`hrJCh$; z?aVlcm79V#&sXTuO1eDm^DLUhOo0H^EDK;a$Uf^?Hd7d~PK+^AP|a?cv1!(s@iJ4q z^adSRcB^I8=RtomVjHs><;j>ki7`Cu!2n^_mc>j4kQuv@)novgv9IR&M#@=td3LFB zv210=Zu5v{YnJERY=4SoTwR`&vUVAx8GpPji>IAsM}s;ZymlF*Kz{b*~fb_zi$EL)aESF`ka zT+DK1r4?ffP|1r)jB7PT&GJMt&i*GLYm(j3>`INj&A5HW7M~(OO+9=3v-Z@#1*Q#V zi)P!gXvAhSsu24~HD;^L6P9&3YyVRjpj^!EY{u&GV1WAa$3H=kbz`>lr$F{%vurZs zOT)6_*>Y33%|kJ3{VCg@lo&fT4rED;DQ3~1T+6a%(W!B}DVk=DS&as~(lmQBH0#Xx zHNh+{V<7tmFssB^RtgehKycr24SFjItN zk)Lcdu3GkSRm!@}qrBLfadl>e*(zqmvzJ}QEgu-5s?D=%*=McEfU0H%S$I~Mtu_xf zWA%Bi7hso};-zH9mY<4d?O9BhhiRT$-Q`g&tJFBHS!Kpaj0>_7pf)o_>rd^bNHL3N z@!iY?FwIk#_0Be7Rsz&oCeyX7i`nH_v;oSPHDJMPU*EV>*Scs zI4AxmSa$2omYL$w)tc>Ewmgg6^8sez*syHTY`13PS=4BNG0SKGhho-S7I9OQWDLz( zfV#@XECwhbOJXd*dH}C!R*zP@2_Kh_%3(dY& zkFe}&^I%yNW=V|6jj3ff%Oa5NHH(X8-yf1ifkJ+2&*IkRk^a<7OEDYFay9gxa8Bvp zkA2Rb@;4OA7R{nRTWyEfoF{yFx z=1FS&sEpt3I;~BsvsosCWW1%u?(=ZXN9j*M*46B5YGl^MESfQxHDnvJq{cPo z{toV}tib>fy9rP=fhW+?<*5baUCn;*10P72C%JJ?1~3}%5w5HZXz@}MX7MdH&z$x@ zPdhS?`Tk+q_>3uLALc?@SSwv$rW8YcntDz$q7i3><1C$eEv1}Qj z#Ili}+~-L(E69==yUU{s@SGS6uv==ZCQr?|n`YhQNozKyMf4{?tJ#1yQ$&E0`t-qd znO1JoVw=qJ7-#=et)>96oYRih;n`#GD*Y)6R6x76c|x)!K_xG?{$vG8)hzl`X~ry* zAu-OD#b9=;W)0Y?X61mgWGu+yGTtO(3sBOG5ugm&9+~|rw#l>sN-ks5tRWlCc!R7p z;}@~A&9giRP|}Pg8dJ-@V%M2fDGOp#%x*Lr#F}Q`oGy4A~B;$z2b|7Xk}(2QYsR8k-7|}2k*QgCc>-Fw7S-hevgl8_|4uA>vj*^-8Nd3~ zbY{2F0P^C>j4c^Qe%fWmp5=*TtZH_j8N1H|V)ejGu~=4^Rc4H7(KO4Q0Z05#0c|yA zog9Z`2ereq_w-{a5>yNoQlCCoX|WwpYR%RyAMsM8&C`-`v8;3BYV$}lc3ZYEyNX#$ z#+%J}vn*bWmqO)inJHjdQsZX`ux|52F&58ar?~nmuUUj;Az8I~YBE5nu>l*%nrEqH zEkK!M!?L)Hu`EKfc2g|K%1dDbR3KY+3e&9hC--^2KE-S%1CWd}GaLO0o3Z*lUCYKy z0ncJGj>|Ya86>OO#8S&zF_vX< zTeRqvZLqA>C%QbUW<3~ymx9Fj#V>t{1Sn<35sfP`&NAb;j0M?Sof;n$vjS|2S!u?; z%w6hJ)v`Sr;KVqZu_ps`0p3cC8?vfp%S%CGY{VM0=*7@%^K2lSHH$!2r&MF%bs*ct z!EsOAQ~dhKPn8@eGd9Pj&m#pYjoGwjmt@>FW5(QB+E$R&^>@*qgxMbEv1L&L)K}yJ zyxivQSsnoURIO|a%Tmi;cDZTRommV}fvn6Ffox^QEg3U_mu<$}8E`$GMMKtrMSr?+ zHyU8YSOSzXV^_0ab|uD;tP^8v#_sYsF%D!k=H9ab#;j}EtugCjwiZA|F(x;LV`=ly znKjYkrRdB!Zi>aS84W1U;;2uJ*2#^}`g2K$^(`N+XHBwU*(DjPnDxl4Se9!(0$TK^ zN9oa$u@mDjb}5_GxZM;nQy>`&v!xkJenK<87zN5*p7@`tmQ7~-%+#`iY$RiLQ|QS4 z#QJe1#-zqVEQ&F!QLC8Ub!MFzQ_bF3(>!37#2Col{Ay*!0&H0pU;nz>lpB|1tUga} zbI1O)v8)nfs#zz-(Vt>~>dtH@#s+N=E6BzGW>^9(xwuL-FssDaZVJ>V+B~LN z^ruM1bY@F3t}f4N%nGrZa=!)9IGBAM^LM6M2Jje}rI^iN01Kd0&YER&GxWROS;IV@ znEl3YTB}jxGp5Od1eK9l@vP)0+ZKIg0Glbin!;V4kC&`#F2s|2)0$ zJll89{2hHBPs}!E(VmX_pBS1&eyWRkLbI-9qd(;`Evd%UmsKsR)YvZLFaL7MPi-?6 zWS>l9R+%w)z4($#C}wM9Ha27WJkNaQGwS*~A@*6VKS?pZnp#%=Csww#%a}U@Xw5ca zU-D8z*7_5QaR%_XGGL9&f>>w9udR{U(x2RzrIvk5IcHDSXWb@>2vT19sDk&9I{xZ$GKd_RKxKuS=6hpnd2=<)lD> z(#p29d1_*oYW9)t@^CRUEbGQBeV!!7#;g`VrOg9m?SJ~R$FWTY&_0307%K&hSpl{n zTX`|nY)G~cTW*R-#?NwQ>}4|2j2XbQWpR&Lv23oa)SZ=KSz%U!v5q>kAXYQ8ukbA& z4O!nAfX(*>2B#e>{)*+|DHjfhHG-h!b>j<;Xj9;I=EdHmJ z7`vFoW4v6(Y0T<2t!CNKtT1c$lR)dvEHwK*%TLaWYc)kn#+?^eIqOmtrr0{OLTuZgx;a~j zrOU&m0ZxqVGPae%klnHDslGPg5U_*U{ZA(V%h@2CyqH?HGUG+F<$waQI=g#5b^;VU zYnDZQA~AlPJ;q)p6aUkbpZp{@X6f@h^{E%583(fJ@{k&vWuIY|waYlQtR-XlpOP70 zE6rGfF+HBhPd(*cBeSVx%ab;b?G*0uxG&p{S>&fSQ?Rj(2b%HF?A-u6m_5Mk4zgeQjh!MaE6KR3*(AoE3jnZ6jlrxT%b0sZ z*8V5DJX{$-EgSiXS{BLJW(s$CUJ(D2SQh!IhIy12GZ|2&tPl%kZ_-hdhmlz%V>ILP zQmE0B%=k7svoTYom=$E@fI>1>X6!>f+xpY8EG{#}?k;yvxeK&u%&MA2e^QqRCk0-L z-DU3hpL~^809#$2Y4O|*w)-dTDgS%%tZCL=o*1A4**Gc6O(De?$+$D))}K7hnhj*}Kc$#;DeGm%QjHB+6k|7eKy7&zb(t1zS*;(3W!L;2pd~Th zv+Nb2*}|+7V-TxS7Q|A`(&n-H^!)39tgj4U0FP?6SQh;$@>8=cmPKmWbY`6xlNx9L zlgt#&vQ^A_05AGeZgXeLqA|N$v*^sYR<;GQ?|HAU=1F7rU;(*kHiH4qj8n{hq+G^^ zYy_y{*`rU`d3iLbLaj*lA=ItRUMg8_2#SnsJ&u)UxjL zkQvvMdxm+ooo!ww^ZMIJjcLq6vtCT`mbd9D3+g8&uBVnzd;8QV+2-8?`x$2Q}5DPl4n&>r*iX-_n#{O{pe`=9VE)?7fR#%5Wp z6gthbpZ|qv@*o+*vbi&W-fX-S)UtwXnJKhrQK|9Tj7o3TQ|`2ROtXRPR?Ifd3bFxh zK~^-YVzx44x;%1Im}bGO$9W_fYn!n#8_1?JOK(;uZVDH(o^p4WrxrjNv6QpPj4@LH z+2~L4Qb4lFj3Yq3TaYzi_xqn<*>8Q{d*54#O^dB%4{D0x(fX@+@#Y({3C89yQQ zY#OtyZ0if~?v@#2S=?AQ0+d)5n)NlaN{ofs=UIO$&3I3HY}QCY0Sm|MPlsEY~Rd7ElV|P z0~C-g|C9GWIWvADmsX~hRi~#o*43;OI`%4b(*Y{)K_Wn`8%&vrGm z%@o{ODaBZ|Ebb?oJQkqTnN?!^%2%eCO`B)wPZFSn*z6zA%{)$xGtGkl)roN_#?0R- zH~z-Xi?_xs2B;RG=<}c%TQOdy#q5#+u}Y1tKox9le{sqwYy%&yif`ja(d)v`5#C&<3kH}inmbY@-5GR>pJSTwsb zVW~{7v0#=l_p?7~Px)(4HNEyXADeLo11uT4mVLy&lm)d1r7TtoFbmDrT)-E5 zGQe#fE5;U$)8*M%R)`H|jad3T>GDW47Rz=qdj*=YJ&Vu0GR3SRE5rt}mW&Itu4S)V zki}+Po<(axc{1nDD9?K(Kb2*XLRKW}Wio2?crpO{6Ey2nc(t3NYuQ2UF+ZF7X?v>wG|dLG zNsc#~{X85{wf|}9Pt}^Ojcu6?ke5Oaqt4?K$p`=5kZ%TL|rG0CPe+mJ;w zj+ugLc0pF4eI948Khc;~Y7At_jLS^{U}^JMe-dI>W{m$yQ|`A`lgF8HAREnCsc~WU z?wN5-1}HO5W7Z^_S{9O3X6(^`l^HYUPAyA0OEF71Ys3n(*(D>6)y%9~v(}%S8V}2! z9kiz!tNqQB77MJa%X6fd6~}(w=apP6FGUx#Az6x9L}RX*RhwtkvV~b2pxD_a3zS#3 z;WB;_m?@}c-}m14 zw`SbMY|YF9*}`l{cGa?>SveM+8V_X84%z9?Zk4Pci~Y%}aW{GppnRMAL1x?&cxjaYYP1=<~Ey_mvX9-%fQ%aps0Io6o9Wf9O`vfJ6#mPHV|G~?wmrj`|A zQ_ODrZCM~GxlIW^?B?vHfT|d$&76?_B2lmP|)l#PuF?1 zB5=1S@Tq%6{7;}Z9GiQ2ytOT@*&61#=m4?RnXOv35lcDSU7jza$pg#wrIl@fLNtz< zA~cI+ti<@T%PzfCkWDok$v8e^nHD8LQOhPVj+w%Ubv0}IlR#_BBGv5b^U&qlBx89N z1z9J?UjNkh-${W2vXvQonM@o|!E7{RUs;I)r3>)fU|BG`^rzf2d!1)_Vp&vX{9+tX%T2LpR*>y&#?FkD7$X@QvNltg zXE8vn%@j(FS=)@^S&zA=FUvd+ zs_`)FJ;2p(I!ZgI?qy@PGh?B4Yx9(6u`p}Uva=1MHOq=-&vwnMlH-Of>z|5dS&h2d zJWh@GiLqO=f-H&g<(DtWsxwPstOexTHM7F3Nfw&DPX4E0)?FU!PiEQWe{wZzo)yjN z#7$wI?P4|qcsQUU8owFM*c+gvK9!roo!KPDOMXItvYSGsEQZB2X5)WyrzZxeAhs-v zZCbScM(lv4Hsgon1yJ;EF_!67}eNQ?vLzh)|oM&m6;-4 zo~Tbnw3M@pWftP7e+p)W*um>P;FUKWK7~))OAoDT_Ms1Zs0-Py zW_O@1%qA@+Gj5-8+C0vTRmtjMH^qW1>QiOLq{h8}{P3C&NyarZOJ>~q6aFWz`3Pj6 z^{g&tuW^^>+UGv++UGyNt67(_xtd2bOJmlMEuV2Li>_utEVBV68ApJkHM<~7Yc`m5 zF&o4(<^FZ{EWT4`Yt34Ik^-ff*>7Rb;yyFBox+eMGqzxCo?T{&5AW0`){pxvnw>7s zVD=t<5J9tOPd;!gL~Fn*F?O5B3RFloF5_gzk0LR)m*PM&W;Q^TY>&)R%Cb#H6iYE1 zmX!jvXW2F9UWiSXM}P&h2vE+9uU`6-GGi}*62~en=E?wS*|I-LGR8|`GsUW9l^Ta+ z-R8kfK`p!2pNeLYjFlF{vTw;U8C?ynLuL$OB|qVRGRvA~r5Q8lZvT^6RtBi|m1j}q zER`(Rd@L_T_K(+%0f)70u`Dz@HsitU@tAdH+@8fES|sC#gI8t8c#Qe)5VJ03ZGS>C zUYPY{0Ew}$40x<7*>3X)uxrht^I}WJuxuNk%(7RY824sW(=0Ay2JnK}?2@6JbzbaJ zmdyBiFx!&>u`Fga#VfEZYMC*Z?FCSpzbh|=_CKv+mTLA*Z+fGOS?9$mWfx?@teZTp zW@+*?W}(@$*0KfJI2M~`VcPhN)tId=Pp^{+&ko7n3t&%qZcJheRQ>c_pHM=Rs@+_(|`}8X=7igcUK96^{ zL9sz>*RsBvCrzIA885-OGh-logMPU&>o$)|*>V|sG{CiN*^Fg>Iw)q}68(uO_qWOa zWXbsJ-JGS#69LNhCpBiF*%F{)fBM#Mg=O95@%&x;pVpka%oK*KJc~e9H}hy@_G6_# zQOSOXo|4n&xfj?TPeK>^TU5*1|5SPL!yXpQR%YCg70F&?HwBPgt=YrWIF&3sTZ*w- zv(&P*c~r_iaSz$t=B_P^5uh*`qd!$K3(G!(Yd*+}gIQl1P+guw%tEti%r5_v8nb)< z6O#cdW=nqZ03MQY5@Xe}DrSw?UfK4hH|sL3H?z%H%Vge8n_XU z3bWt*O)6&JlPPyy%yW2WWe~gDvKZ4M<|nUz5^Pn-x-sh}&j`l%g4KTWg^tYQzdt9& zCR*fTHD}%B>0%bam;=pLYV5_RzGs#ukBGMOVoe5sSB^>cabUI(i(}FIpLzg~%iJ%1 zDpra^npKbIiYq8)bv2LEV%4&tSy)z93Uy|$O`FG-MQg?{tZQcDSTthGvUmfI#ocA@ zYVsJfs%2@+-invPkQK|mVZ-cFjQ5B&V51qQmNjBGlC8mjY^Lb_Pne8LfKrF2n>@IT zmt_%-#Y(ZOWk)k^VBW)@$3Lep@VAI&L9N^rSQhb9;C|8}E#9$g_jt^)>C9G_r!iZZ zaX}U{MME|;OP7blcp0FCS)E`OlI7F@o>+F>3~kJ+mNjN=rjY#9{wH;L#Ix@5>{wRw zch;YL&n%^Eu`IQ0(X26xo8omG^K3O{@flk&?kRWQf9F}AD8}!?OVR!(Jd4SUB^i4$ zh1Ne=G2WRe+GGr5Ga1mJ-R=xXW0u5NN6*wrjqd@je$xex%wxYjLzc8Sy75-Zx{xgc zRPn6bPkNYUzp$dS&2us5?kxLsu`G$P+C0!K_9u8Y zkd6PzkX2*W8{0^Xh1pV{G8RCaN7by(%U_OV5y&btW-$ejy(N;dX;vf)WN*7It5MzM zk(WYO2H<~U{H_&anT%1Osxb>>nZHXdYsI)RyE?PWOp!iMHluc8OlnLy8;&g}MeULa zXz%SQd+OhQ?34OJe*+wAo)vD9jLox>pJ3VCGHcEFqU~nrvOmGHtw4E{XMy$zW}l!g zParFnHOW?&$EmSUn_^ZR8~YQ8g=Pg=Ix3it0w~l*`QVysN6H#>z@qSRI`oQr9aimwssjue=3^w0Nzuon9T)v z!mN5c;@K~b07rcjf|a{*C|NsVv3@y1usnXSP9 z6|*<%m}TAO$&~?+Y$wM1S)PU2cVq!nC&q22=*1M><)NBYTFm~Z(w}Tul$WBa*)@UJ zQ|@Z=sLO-?q%KbcC_`4Y>=d)-@FVGh2IoC|b9?)fGh<6o%8V^PMKp$HFXC$GGhnZ?0<&Gr`ZleCEXAzaJf7vL%iPzL zyNlWDuMcENj9tu9%L=n@^0WZu!2l2|mUW-UT^?xGlCfAekfoaK%d{wF1z9??rdf4n z%l||zyESHQTD<#aRMKKS=*#Mm7^6T%Gd5zK8@FXqZQ1bbIXqwIT~4Rp_@R-E%S%CS zY?eLXSyQY*>%=(2JSt>$a8s0KTr|6}EJL$u^Q>u}O@B%)8~+oM@g68YT3`%H6LQxb{T`&?2>V7)~fM-0MABa6k}mF z#VogJeWelWVpf^4y%foe?OBWg%C+offRbnN8r|mZVV;_DcQs3w2LTG2wEzWTJ>_ne zHDtN}t}rW>O)0C~7|5>9tlK;^d1&*gkeY2;acfm^&<+Vzzz8TQO@3lowOfHkoZ^c4@|6`DBjHW~<^i(TT-%;SIbv6QpUjDuK+mNw6A99Oes z#>VW9X5V1}%6*;%*&wzyQ!q2TCB}F2KcN|y`n2oJ%4V#CUYs@$nC%VZ=lXn|_dAW4 zrIgh%UhSr6$Qrctd2kt1%?7c_j6<_qXSOz@Ml}{-l^J`_;@V8H>&&7U9~84y%et7g zX8epRp1Ews_CF~zUf0aBO(wN$^d|{Wl^DlOft@17tRRbK+>-$e;L+x}MQaw#vd)ZC z%nGnT))(NJWOZj{r^ZOeSQgQYDP>*F*7#jH7M&Q!0OdZ9%h}Lu`HWfF2Fuo7RO?S* z){1fZJoIKuG9D=2hX|ZKfxJ*(yGx88YP=#DgW0+o+O1iVW9$?Zvo*}4)ENEAnsF7g z+DyS@z;2i)izz(J^F%K*w#zteo~L-uoy7R5Ysj5p9!R#@JYV%yx|)Z~SY`?kdsX|N zN`R^^PfNzve~ktMuDAXKV(nQxgsc!-n1yGP7x!dst8=D=sf28sy5HV zuo+X!R+DFI^SGMLIFEUjTV}04fmmVIYZgh2*JME5%(HDqm1r!pcoS>&f3Xrn)E+hj5tu#qfQinM0Is~+aqk2*7Um&Yty(>$XY z-}fIw$2_+$$X9z{)|zowvPH9xSTt+MhGZWp#dufD8nFwqWi;ky=nUY+OQGH@yJT=v zEYG6zVj~s-%K8(zu}5YrF*af`Edp8TPtO*~7SHb5JhmC{AzMCUlPs8Rn=zf)S4n`f zWL)yoraw{4x-+|I)^qM%&DyfqQ|?)2i~vMfLsx`@ep6Bn<=W$w`Vzy~Eh_#m@7xRc_ z>pHFO%a;Agh}F)vNXDXBH)hucsHerVC<7GwlQvUaftjMFd7>C+mM3(Kz2 z76Gg{_K;?s8ml*JlI_$u@)LP+IiOlHo{IK90{6lL<~@8ztru^p@p2i{@5Npjim^I0&Hw()OvyqJPKQS5r zVqMLeW|JA4W`V5NESCP1#5kQ0&h>-oEqj7g2Jwq`8I7G_1V&WyXuque+wTer-*n01rKd2z1!5MZ4c*PWGhGtcWn zvd)X07$-AEe=^O&v%+j>)_F0zDGIaRX6(jn-+$-U?E7_eWo1FOY{sx`3s7nDFwB$v zPZpr&W}f@`^XDE|=|X#xOW8J4STlB)N13sTSuaN2DNyn(k{BOo#x)pFBwJk`VV1#w zo(v!}#ddZ*S!Kp1*#@oqJa$0c&nNCF{;&%Tn8$xVVp$oWyh{e@X?12F!67d` z5TJl;QsYOtGmHFGHUt%#b^cS^J+dfQMs|auxwfBY3K1 z5uliwO=A`b>J($|{(kGnKgTb$x0q&A$<{WRhnJNCp4}C*Rm%#ofR@!1)}M@7>`(Bl zK_3& z#xE?*IFOA1^|F{LZdjVJw#m@tG0}=+tv?|@Su+N+tC)SG083s>F^d4D)+{XR#8|^T zmW;o_H{k6s%Ul2~o2_ljvq)x~#T4C`{dU?sAL{+%_@AIz1Smw~eV^z4K66j+$6aX1 zJofuhIjg+bsWFQdMYH9AGRu|#MVF`TPiEP+Qy>|KW)0ZKJmyPj%~H#LS=F-Ej7f|o z8Iv1_W}otuue|svUCJ_lrw56#`aGn?dU%>gxiRvSHdA1LYRTAR?$684Hs7XY%i>a? zm~(#_l`L(Z6tk{ot20|Pd+V#kvR0p7S1e0zZ2c*i-5|>XavK)SvNU<@e=5j^Wp$f7 zEbBH;n<=1KW7htsZ_{E_U8dzJ_mJ#Igx5wanz8d@4Z3s7$MEcZ|JifwbNm8(gCJ|b z;xl$;3}{u#f>@C(G;94ydhsJkiz_oWW-UMEo>`G>Z8HY4-0}g>e);1bUu_<)tVA<* zlP9S$m_>iW|FjZg?UK11z*b^hW(olNoaDusa~ELM=SgDB0;sQXYgU%Uu&h#J-59`? z0aeU$Gmkp6NX7?~0fkwOxyMcMhFr~4@)I;GmUU)q&|-f|YgU=@(x2$edT7>(F$>7k z=W%Kr!C09w19-TM-JAVzb!FjM@?!C<0lP+d2DA4avZqEjHfBL>ldL6UAWJcuQkJ;@ zJuE*}TD%ft>e(d5Us8E7nz7X?%W>@oMtie}}dU;$K0*}h9l z{wGVuOy40t=^@ZQ{{^~dR`SzVf3+5n?>viZGGKLi(w9YkV*bv3S!>3xO=4`ohGRW` zM_x>3oV*w_g-CX7rfA8yS@xU1Np}WRXSNnVsgl)0khNs&VzxT7QJ~^kbS+zX@!cSM zKc2DkAr}~(p8eZ#DNAZxjag;IZp=cnu4M(<+Sn$@nr7+oP|QB+3m+|-bzt0(fW5(PW=8^t%09o7=U)SrOM6(-Y zWu|ar)|ho)wi09UEIpnmP(f_w+#y==EVsF%K0&g^tXcNsA7}kjDaMl--=CjAXMd7k zSg&1>O=2vn4au&|*vn+-%nGo9tczLpEXMz22UKVl%&N`Pim?Q!Al5wVYL+I?lb!_2 zu4OXNEQzr&`}FO~fU+zKvqG$@S@E6G?UV=m??mgUZX zN{oRlwd@B!_yIS0*4;d+X4U2SaPFDi)v^L>1Sk~a$&2p~VD&K<7CO)R-3YSLpIpiY zvASh8)hwl~GvoN5_MlB(-1B#ZSV_h`f9G13J1ePWMX|O&r8g^*eX=%F=w=?X><+R* ztna^*{Paw6W3w!feU3Jx#!KP-Pu7f`7*ov}v(=dev&c`XWogacc!Mqt$i%FQS?R^T zWp)qQGE*3_+{~k5w(qReWm?IMVc8a-7Gk^26P|6z#%1jByUnqf&8XQVgJaQ6o{#!w zo{(%=7O7@4GJ8Ltuk&H20<$cp=)Ab~r^f7pECQ4P>%MGgc2BZZ%%U386}XITe=5nC&g|;Tnr4OA)}O%aG8r4Ql^A<6ps)F` z{she`Gp3r=$SgeDUW$^RYRny?O_N9ZQy`l@PaunBQ3j~^pGthfW*m}@Wl=O+JR2*8 zSqCa7#fMM}Jf!gZKnq{eGNsO^9Dl>*=b;PmFvPzAW7u&L^ zwAlJnFbil!vasw6UkGG981Nzu^XNABH8b0paS-d&ScommR${C+4|%Z#s7wY}eac`! zE5`23R-319(*m=e4CvIDw75*h?~|FLb7Q;|=ugHhJWFcqVwU~mXvT+_9lYK@xcZ|m zFnFHj+X1lxtbgMMyo|X6*g&>KW3j9r-Q?+=ZIX*0<=fmXKvBsmH5Sbpv%)JTI z@4T7<{psjtielL}R%bStO^*kfJyOi}#OyZ$SO)OmSmy5p*)@R&&8n33Sb#z6E>B#> zpmtNAJjo-`m}1t2?2znW_AFRkuPV@5Gp3qdjagw2(bsr^rjWa-ON%)aBD@BI3Az4M)QnLF~6Q)7?J zeoIi>mc=8T*%F{SHKv@ko1#J6kOi>?+RxxAd+I-QJ+m*&zZkD%#+4e|W{hks35vl0 zK~|vk#XK!PRg=e##h&FsfO08o$ckhit7FVE%F_arm&qtGX8-saa#xqJbqo~y#LU^a*)Gsa6nW_*2R#@VuH%*Oweky*SHPK+fPH)IiwGcg;<|ZRm*Oe-MTzwrl85=S~fgeT^=OkO@C4~3(LAQ`=Zi})0h>} zsyDkdW4C!EKf$ultng~jqCtDx>)(*hETDZ$AY1mQB*u-|cWpQGs4-iJb!OZi<0Jm3 zPK`Z*7yHvumvLy8#2CZkckpZ-%noK3NRR$ymlzwfUCn~qeV50ztjgKU1#C8BI=z)r8ph|wSXEFPqeE%J@JT^d~KdH+T$TEOeommtpR-<-mjGaO(OI92+ zMH1uGvN)hV7B|ISF?KaOEc==L)VaVgdid>BW44Of=ub950a_!ri`itx-IjHery&bs z3$xXjt#Y>fPtu>#=-I7Cy;ye!M1B%zB^e_>*|X?r9`h__ilsl%n!V;41SluQOELzr zB3b*NY=By33IVoC8GcUiuSeid3`Rn9X1SnK1&?J2f-A%oJhSx-!6n z0onhQtD!@)u`JeTKoa9_%bI23+2vTQVIETBsb)Vj!0Mw9pX3+POSMU+^(T+>9M0cm zEkzliKEI~ieKE8hivn$TW<#@rtWoP?7SAHnJO*s7rg$<(mPPxY*x2?|E5_zo&)*5M zYh*U5adl?PP0_b}BsCVz<~DZ;P(mzT3KmmP%_=i?YHZJ9b!Hh2s9H8&3QNXJ%)VZR zMFCc|?3)>LfAd@4@|L${G@#5BYXWa)GgdK+X^}&thjJE@z&?D%Q$_WC{V3F zZJ@RKWWG0Xhjt}`2&?IHKhjEz_z>(*>At8#W{ zrU0=jW;HS^o{eoW)hsLbSfjm`M+u4Zk4LNV?(Pex{+x>*((atE-?%pO3t6k}3jGUE*MxG@`+ zEzhD8W84&xjLotyk7%4G55;UXW;M)nE0#r_o(x!!Wi~)8`_|HoB|y2&^G+RQ##tt_ zEQ?uY9F{G}RxztWmfUz{#@>r+yn2~T8KCe#Su^g<6xN?cGX6|qdyh1AqTi@umO1yb zQanulr*c!2WL$2F*o?U|U?EmStAhfilUf#+ak(iT%Vr8=cE4sZj>UKu#j(+!)-X@> zCqXut<(dyyvzz{O_0gN54-(_+d&*sq#k5FXi~#kDET(vs$K0J5cWV~Rx|p?Nu`bhk zlLqjX%~&jpo5GmQqQ!&6*p@}D-Q)qW0j)`vVIG&Vxd5*qt7|?U zFTuEt#*1YOv#@L=^eLS=X{XfhUgL^rr>cuxwrPq0Bgz#m4N8X4U4wOi`Z24_bb5 zqX)cVruazYCv3)HSvx6)V;=yt=2P6$)Ba??&7jSiMOCvpnGG;rw=R!RD~i?W)~w3e zFEnCHe|ikHZ1z97l*LS8o^>hPqXD8>b1XBn)#g!Ve95JV#wf;`z^lwS8!ImE>>3Sd$VN2o)EEV-yF8N`f5zB7*|HbAle38GbQZ2buXl=5OSV@NjoQxs#mJnUIq#jNkFjGMxmF{Z`Nj8n~C z=EW2xKZ#|*tXXzJ)_tC|dBn2irjTO%LfSkY=CRG#omukY6tn8gdNb-Pl^Ay|Ynt^v zAIghKjc=1^EXeMcjt*fa}d znREC4rzpmyKkXsglL676LbKAJdNjaYo~v}O7Rj#8tmLQK*(Stl0TeVF0~GQT#jG)F zl1()m$gVa|w#mFkn9XJinJEO>ZD*SeP=YLyabtGZnQh3v+dUqNS@CQ#W3O3sVoYkh z^e30H?(vWoS7K~8MW@D?7ArL##D2zzy-@2}KnrAhmZxa8NS4gFi`nGG7sazE%u0MR z&2IXW+dLc1rq5HO0V!pfnib2goo!E*0;MjGGvjzE7R^4RidiSd&Wx`PW~CU*WxTQM zb=P58gk@dM>Si8eHax36izPsLHH8!7s$~t?)UwWuVc8wcN-(A|tI2@qPqr*ZG?oID z%e3B2V(iVRHb4cl#k21>&#r3LL$eul7ixo9RkNG_$w{$)vzg)nc*4%7xG*)SK(wv1IoLMJcosqIzL@2rkL9LtlLwxa_Jq&aW(rlaYRnq52v9T3^FV?2#N+dB-$0XW(QK8n zpNsm0{-n#aK$?QF9H>IpVeWHZH+S7v;%Y4#Ew zA=b-etQiAXJ)}N)m$&wD{2YXxdI8c;Ng`V{-q%U^zzTeEJ= zvVOc$V95Kpsgi~Eg4TO`v8IVZcq6W{|aGtD`vL^P=Tx#Q#dOoG4?ed?($eP zRxPUsBeT8DI5f+cI~;4sN-{<@#{Q(^*6c>JPeC$fXPau-wg1V9@$SxmXvSV917s^R zzV^A#yY~5!jAf>y6ll`UtUtZO!r+ zvKhdGWQ%5j?C$zIX-^ysi|)0+=>j6BMvNy|0af`2+ z6=IFq*VCB2?Y2M`D~0cJ4`fx%Zi%rqV^y2Z^XHqR8Z4#-0qI%kyH6HdEBi&?4E;Y%bHHHM?0Bxd5+fSsi-p}8OWK+&=nXxtFBH7Ke=qdN{ zK0Tn2z3}?8@T_Xt5N%r)yO^yWj}l{HR*LavS(N+~m$5dZk{XL<@jn@{rdj32D8{Wn zshE8t`^Vc#5&4OgZ5a(vUsjmiH)g42uT(8tCgYVFQ_7lW53a0)WStsk0B_CmumMW` zr%VQ@n00R4E@K2^BxBL+8{R--jQX^iJfd02Pn-W~St-P`Wmzo!srs_ze|qnG->>`c ze4D!zW2?q#@>E{D4F;HH<5^T=wmY+P%j^RS*$WJ_PK;xIaxJ?oi=7%EeqoS&4C~+2Yx{Op5`$XNhNZXJrH^idm^owk!&?(Vv!O(JZ?lOKVouY~&{- zWBgB$?9I9Vj#`#t)-?#ku{0>%GV3l6CZ?5bvGFyMjxDRUyRdE6JBE)S`3%ukKkhO9NHtuu>eZ2Qv}2(TRY zc*u-Jv*y{P#<-sh*&1?BUlyWuW{ms<&sHtF^HNwcUJIZAtc%&JS~M171zBUZZ~34# z`!$%1lNc9fUl#w=QUijJKItXU4)T)hwM^v1|k=HF=yG zyOcF%wf?F4JlEK=D98%4u^CH#TKZE#Hi@z2r;sf26V$Rz2(77^iU{ z>%jmxmLvIzVzwcxWin>j9thgnW7v}kA-NH7zeV_pV%eyq@_SL%YxZUFAZev zF|K0P^LNl}cV=t<)3w)b3!q-WHM1zjwHei29!~~rEQ^EfGPcclgRB+fY^I<$YsiurAMh-|=i(9kZm`$7KHtSFEQUF;?#&4&VrO#8H z*}5`dxBm&z*yDHBpXxSuaqOOD>C3j4VlzOQWDi)jFzcu6sKyU8a_*rNeWvQ^A_{gccT@=|neEXXP^c4mAy7~sVevKeps zlkHCh*+wjVo|?Z~Z5|*ilHJ*i%S`bswRzfTECI@?@y<f2Fm#2zZ z1SluQYV)8Nm;I?EqLl^G*HMSyaX=i-ZL^F%RDV$5K`<(Eq{rj}(h#k0(^?OC+VSn^Xz#x*m$PmOUu zaRHtnOF662fYq5*UL44JWcJoq_obB!vN2Pu!t7HB0v39y3fjW?QQ+p|bw zi~z-A3Ok@kj0M`#jGY+2iJPIdWHD9>XjaoaRI?g#_igT;ze|^g#w;}Z&EL$*HqoqI z#u?_JGYijxS$h^O85hr*WW};uHM_(o6sYs?gq=@$f#k+vS(vs~$<+QQShiZTSSfT$ zGlpd?8Iu=-S`=gVW`S(16d7}u`cwi`DaM`)FlKvZ_Ng>xLF~4fLOffL&1MREvmn-( zJ(3tlGp5mF{V8q=F2F0z*om=dwrbhvPsmTU87nhRn+KP%IiRwJ{S<`K<$GGI`99)8B0 zC^|jni`}bFWq@M+lV^FH8M94h6|}G%hvhK{5{N&U)KI1qR)0?Hu z6ZuItj^wT)}k>i z>teQ9_D1`Rk)O){lxo%_o5Z+G#!8HRXCrM)w9cId@wU>mMzJc)c6QwQ_O0b=QUaY708kqqZqr-Lz4&1Sb(i+)&i6_K#>|Z z$!=zfr5Rf?rksua$*D2x$5YMc^rf_EL+q6nD-g_1yS(zzHe==m7W}g1XcT-fG$1Ll- zICcu>#oI6sG%J!d&nh!UfAX3|<;Cr#P&unEPo>6&?ADomu4ZNhSWgBRv#w>yjOC^1 zn|a*kkznk^82w49aZLs=<{tS;lJTvteT}a9XvA*%lX=!Ot5UYwJh&;mXYsGT%X=2f zXI!VX92-UdljH9FNI0g zjUH$=joGt|xd*a^*hRCRziXD2|A{Gg@22Qd7M2yyW&ke>pf1jB?rSo@Zi*6(Gt6^k zirJVcytd7jMX~HIF{YZm{`%}}qnb5j-R8keaX1-pGkqR`_EtA$!?FQw5@U_ID=}7T z906*XDd_TKHL7!C_K(MAyk90$JKIQ%tr^pq-6h8B(tvKv?gS_SH+`Of_WV6%PyJ^; zX#hR^dXpCmvH@*)_Tir^z&bMqwBFg~KF{W*=q`_0Hhms7W>v{<#jI)8!#s3mtv{ud zrOP9lg=L@OzAR=6)v_X4GGh!-my2X2K&3NVT^=RIZq1@U0a~`nP|G4fG3Jf{CHYDE zQ)R|4yWwT68KXaGETHG_ZoY**i?Tqy#@Ec!m|fLu5A#$ti~Y%+*=WYfjOzltl^J(p z9G0asTWy|Tb{m>){mIpA&AF2nQ_NPM2gqW8^7!3!dCuF_VafilTjHdbR+sj-(C%K(M{$@`xSSy!_WpulVo^RWL3 z|5N3~Tp7TnmFQ13e^-*RFl)%t<&l}<(x;1OFQ+l9W0sZrq{O&2uZC>tPhwdx z`z^+-7m)X2REk;bPuf3j15_!-s81fh3u3kZDWY-TrDdP-Oa`3aA1o)2S=U#r#2At# zFNSB4jA2>b%p*5Nxu3wSX;z7GFbizOu}!lkT2U-4YojrMjc0M)%tLa_{dcZqlNviO z<}$7FKPfd#aJxM%{(N=X4z`ZzW7B92BOI4 zpkP_aPt>xc#&mhavNg?fTeW#?fTGXiV%CUlGX=%0+dOTih+=G-&B$!_EWVGKSsy^V z!)y-*cmtH|PtCF<$CVaWDLZ-bc?Io4z{&dyB>RwuSbY*^BN=A{6uEI*7ArAs$l`#? zTtF$tzBHg#qjqCf#VjiL+mOx9Hg#s78qJu*m}>UvHdCmU#k7e0)GlMxr?h#r zWszYXAvSFu5X))Fm~xi=;|19p3$m|}V4PakfPJ+S<6I5>y4O<4R%ccuYyC+Aln|@S zwBDYJd15nu=hr(gmSnt!dA{kJw6m=<?z&Ds9=Cm=)DlUR=ej3)(`h ztJ(AVjGa$@@}RnX#X$BU;n>1#?)d<*U^Z7)w#!&N%QO#dSsg@UCB{OmUB+LG{zQ*Q z<9CluW(;Txvf@}<7R|9w!ArrRmd#v%AS;@6Et}P-bY{tmuf$8CEsG1Yk)LSHrZWp- zi)6`+MYAu}v0|*%sA1WgtU&daMQx^VF{=SQZKg10W2Sf`=S@A#6EDSfGc+7qEURli zN-@r4K)O6$vq)xKEDOa}Fp*G$V~z|idZ{kgIs;Kcn6o~4o{F?K0i zw|qRjA*)LR#IkP8LbC#_AnVMy0BbWvFKz4n;~*BE4b3Vo&M;4Rd9oVyVi&Uv;O$wq znmje-9{GtI@M!TUGlpg{Qv|V=p8{DLvsKJ4%c4kDpp60QrqZ8oCNCy2zO}~Oof!wR z_ENO`l+gh9WtAFxm6$twrgg)GrMgw7Rxqh@hsAp1+pKK{Pbberwy~4W^DOs z>`&(xuYC3sht%~2KXeVvDlG=IOw9Vk{}eApx;(o9ye3(v#_~U%1+wnU8nSNlJn;$X z%T{Cda5E}-u_R-+d7?fs8BjO#sF?NsC)zyM2(!6mR;^i=vQ^B=OOeTd1IakGY%puY zax>2@w{(~1HJ;@WWLW^EzU&*8%NY5|*L(=EY4a3h>z-LwQ}{M5-CEiE$Awu>^FXri ztIllEEQtNKK-T&b8{4Xswf$+A7gNmY88s|>zEQg%aMJ!_l5N45YF2PHXc3I@Ec$95 zAS;dy%`(md&zfbq8oD4G$vDNV5@X+gC!S?4K%n)Nl~@*W88^$qv(AjWmQ7|%E!!?* zc`4N9>BSTgpkP^Q*zL<>}27@lph{f$TH2nL>gwl5s4H&WtND?#?W2p475t*%!-AA;d;9F2~}w z|EY>uC&oAyRm-}VeckIwjD^{ljC;)@h_%ZY(8m7c)Y$WPRm=vm1=?PWnma4Cn8KN{ zyFANgoHkF+4l&tYg)Qij4Q454 zh1iJ3kAC#J08euPAQqN&o5z}Qjkz1LH5d^8lM~~BHeDXy%tM!_YFRR4>rYp<{-nGZ zk`-i?8;fM0$C^cVd7#;H8A~xxoAnnivp&tgl)Y4QlNQjBjx zf7&%>50=S**|02TialmUv*K7-_8soaN;2O3PnzZFWio4*vDX=A{%#Eh!~mtlI06)I zilW(g7C~(iAHp|*fVVg0c@q#R!Sq1}cdp(;ez-)$j zP>dOKPci$Buk!+OD%nkdivF}3v){rt8927K$rNPMnq8(v_jv-@kA4)$9vKZ7lksN< zto5&)G^qOPjcGB}tfVK1R;sZaio#|>th^L}*887a%)a{@)#QoG*nrD9KOV z=4t)u+R*Gln}bi*Pr7DSo<+Jmm?=!N2*&hgofsoPZJM!2);vow3&|E@>t<*mTYw$;>9h26 zC4fd zNQ}X3Z<7gPd&+%l^MKiA*+A9;6pbF*JXgg46~!0@Dl}{TNs_TOV|8Z9i=7$UvKX3; z{uGuKXq^~0W>JjEjBPV^Uc9MKCfS2Acc;ZB+HcU6m9Z zEB{mMPvKdYvXP8a&o*WUvY#c$p1fWU9^)S8iDj`M>%Ef#%c2yu{{~!)~vfcRI{{ZMY7%Ismz$%7&}E4 zQ`G!jjk#Mf2C@QeD^OzD4{|ZI^WylQNR36ZoP}7=1q^0CTUhOX=fv?Wm|dB%i8eN4 z4F)(hPM3$lfNJt2FFu+XcQvaU@VYgN$rzV$5bKFq7qjWiG8y2;tN=@!hm&&FpcQ1R z%>!tuWi=OI$O^E}t!h@1ar7sR@>FLwH2V^{DUuj#GQc+Dn|7N~TQfFf%K)V|j}1_y z#Wz*(SUv_22^m$y%8nW*4^a3b> zR(Dn!vb}#in`Ep&L9%pt0Bs~FXjT+!$X+R)EyM=1*N9|SYj%^2O|!0LtC&@v=Vh*C zK`f&IhOBB?5@U}BxXmM)4Q3%(BSsvH3W2QL&SnYq|q+!;ztRag8)un7W7WIiE(fH8*RLf*+ zG^Uj8$$%o+GC+xC&9e|~idoMFxR}+NMO0(*VtW>wW-oubH&Y0+X4xxsRLpXld&cjs zZpf<51Ix-vAumNkHj?q-G8rS*kZr|S#jFxz6ywa_QOa&M0q{n#U}g0X%nRGcikEoXq%0YW%T}eq5ks{kS3P%($OS^XF$J0)ORXozM9z zpTpyGK8K$*O}K(?Rko3$JR)H1c8a{YjlZC{78$DY<1hJAVOLc9rTcoe{1itg-QDRS zH!hyNY`D0Oy`oY6E=kHHGalFAive;Jlh}Jzj(H{9=^_B zpFY6q(nU4IP8Ub=lKzWlVoQLVP@M)d1(4d+cR_m?AjJB3cXysp0|7!Q=Sfd~l6$gg z@VJmI?a7JpW-~4YibfBjF(MRPOF;|SHr*mbT}xdX_af-EetalLb)%O0wvuIVoP7Bv z+oE0-F?mawH>*Xtkfka|QwG*LRq@7*EObk%as~|_Q}}L)bP+T4Z>v^W(5Na{U0t8D zV|edddPV9M`N0o@6L}(?>zg<{y3+IT@P3D{*}YVs`MM(~>2O}DD=W`-0>=mFcXDtG)DO_-J8z$+zf6C@{lfOI z^{dbR=X$@RZ{`j4{qH_!2c+M7efU}bl8;|e+&;*!`t+QMz(n9gMc}S8{&MfU-GAP3 zCNG|-Z(+XJiNO7fz?p+{^mFjWyL1lTsb4te)%u8jkI(SWWBvg?{ip8qmwFVcgWaS5 z2T$_L^MCWK|M%{?Z0~;f`up_K?e}=@D9r9a%TJd( z@B9pZOw5Nrh$3*z&+L2s3h(>J9eLV6xL-7j!lXGn2>9#n*FWg~yfc{n^r!hB$d`KN z%pG^!e*0hi#eMp@Fz@`Jj=-^`a4%o=egC)zH%Kp#ukNn9K&!FZnC0inop(Oye!9#T zbOIu9fuQ2S{%xGO>&~N@{Db{Vp095raH=D4<__yncinMk<;ImHpO2uBfa{!(PkpYR zGhe~ozJz*p|8cIL?JxKJ`m5EScmG1_jrZXVDUgo)xcvCyGQXs z`kK2;uxIY9R_%lItDG-uA}|q{2uuVf0uzCWz(imoFcFvtOavwZ6M>1qL|`H?5ts-} z1SSF#fr-FGU?MOPm1q zL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOP zm1qL|`H?5ts-}1SSF# zfr-FGU?MOPm1qL|`H? z5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qd5FOO_P?FE>pXng`IHlZiNJY_z?n1W?NiSuo(N0?CIS1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FG zU?MOPm1qL|`H?5ts-} z1SSF#fr-FGU?MOPm1q zL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOP zm1qL|`H?5ts-}1SSF# zfr-FGU?MOPm1qL|`H? z5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPmU@`y~D{`TY7?`hDiliNL9d zfDk7n+;!(2LK%=14DRjwK6AT%-6^!3(MR3=-HleTbi3Y1Z`S+Xd0!#>={xT@{ONJ# zj@$3NU0+`v{|o@E`9agKoG^)m;L}r|-J!PJv7mcW=aHxZZWUI8?y;)a^%u z1jpVkO1w)9`>8W`{xAKhbGLUp`e8?2{);nzp`RrU(%Ye2eYW*ZcbxfO{2UM+|HY^8 z*ywk?ef}~Lm1qL|`H? z5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm$XpSt~y+yCOYudF}O1NrBlozU~iPw3bE_}cI9 z$KA_6^@&e@@)P&+E}!i;pQrEiv;A$Xf6pc7dg<^lU5*~UcD>Xe`lo;LiBJ5=pZ)nK z>Kya@=}*>wu1|B!U-W4{@u$apuDgB6xqi~Sec!I(FcCNz5%`mwlkvsQS2+=w z2>fZ~2NQ{jz(imoFcFvtOa#tD1SU5=4?&$zIT4r$OavwZ6M>1qL|`H?5ts-}1SSF# zfr-FGU?MOPm1qL|`H? z5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPmkIM{r5lq^H2P7uPE=UOyG}sJpk9?jpq=xpwTmToEgmC%m0IU^9hK+5un`5i}&#xI`Y}>1%&tUlOMkOk$>vbhyRvI z_O9FSQcXK&B5=YZaQMu>x0mkYH+$sM-FeUFK7R7UcR%t^ojLrcK=heAgxNV0fr-G$ zhyX-;#~p*&lktCRzRC+T0voYjnBU)gR}+C#9fAM&|NicO{C|Gucl7(52Xh2|_dovb z+0O6%?(hEI-Oum;-tYbX@BRM4`A@(9pLiUc|NNi+^TGKq|3xqUmxJ?PkDNdFgFpDM z`nAp<{=pyq!5{qLI{%Ho{kK2-Z#n7ck&k}lBOmqo zSf7u7?BgH*_;={x^PS)E9pCw#-}%?{(D^PszU#Zb`@8k{?(hDd@A;nZt@C}~_kG{b zf1Drqfgkw6ANau^{K3D@`62)Q!$15(Km0>~+k)&|I_DZe&%QX zz8-)7@Bd$%fAA0g!9UdbS)HHz*`L$-M>_vl51oIa^H2ZDKmBL_Ob?xZ?(;7=|F_Q1 z|NPJY!Y};%&;QF`_?J1qsPnIW@n7lu(l6=pOTYB5ef|ySmw#E0U;gEP`^*29^Y3*2 zz0R-v`+xWEeg)^Vb`FKjibnKP>F=-{1HV0QjT&^&>y3 z^J72uqd)rNKmKF-1rBonAvpZWpZr@t`L~5a9pO;_8HYge?*PW10*gQO(|=cJ)DakU z1V@39BQO>q{{d%VvLIc8M{MBF8!>9z3J46D>V9^l#&79x*&ENXX-~6rL{w>aL|MqY5-*5j8 zr~dHY9|XTWXr8U}h3wEO(0(tE@8zK*$mWP9AxRzaq%a$}3aH~|4MjN#M{9lMj#i9HvA~j|WTVqxrpl!$stx9nX+N8Oa=>7A7yqaYTu>V*jE6fJ60i&zjE&D%=cgal74ZDXaCK=b@E%eFPSg-@2?oM;#sJc?q7Y>3wNsZguTSSfkm_gtplyYEUhzWCpwu~FF?l16tYi)78F(O-y#TP?tYFT8q z$bJB`KFDk{B{N%FOf@T9^#!o1WW%g|v9LvCQ_Lz_iq0NYR>oMoI$$DR`cwfE)c!#U z6VMu2n6=P~R($ajzRWBl`yZQGs#$z7$0Cc)e$9tUS=X|uWdF-5D`)(TB(}gVjZM6y zd`Ta>nyt!8W)WF48>NNWH?ES6;6LXrbor^*_&cERiTugf9%!{Mjx{#1cw&fsyAo?$ zZHq@^?TU+R@~4JR@x{ojj)3LBtmCJhKao8h@~6ryyi&^&2byt3}sIm_p zP|K1C+ZO|^nGLdn*2k6Xj)aBmfAC>zyyB-XKsL2(eX)_9P07|}iA7LAjO&wdAoy6G2 zn3m0kmRgo0mHm~9*#R~}i^@{W$`tFkchkr?$c{1I$W~@4XMg8v7LENRmFyMy(*tC7 z5&Ot0OA|+DobgE;$2yC~#v2nFJ2F=KWR(TlDP%3OU&R|cGR70@l-Uvsux{j_w6108 z-sqEBwyhgivN+=EEXXD_MrJLvZ&)qMf6fcuMwg#{eZK>iT6tq^v0nM{mwfrWeH~e# z)#tk#TBpXg#i?Y??E6vKTI2M1B(qW4$gG`l@~0tI*aGc{ti3TRYmFr{#uw`#vPqwk z7(*?6SD+QL4<1bVbYhHwc7Sa)3$jk1BD3&nWTUYF8=-CZ)M_?eo_1b=R*lz#$f}f0 z_(W#>9lUN|yi|4~V;8dMtd3rsJiT$1?8O_uJ{*6-ze$+3oP}9^al~wpmCWk+rKPNq z6|ptO8jEP3N0qE~_I-sFtmhFz#jGGv*{He|uXlsmjXO>xt8e>G(&Dj%~6|tXO zp|V%e*)kiM4X~MSYHB?3Q;?kmYK6wq*~sjdUmlp1IbM-*m?eM8B*p5>LacfHcFXKo zV`av6#vY`g&qJ$6wisv3L0~Pksb$~9%=qGsUki*c{Fik2X?130jRP&!Y}-5?rkKcB zwwQueGAo_cDSGllo|heH)cCZ@tc3q$2enT7G8B6KEdn3 z8n3o&fGx9DS?TP7S?jFZJaNWKjMJF?JTr+{-vUoX)Ilu|(F$x+}{# zMU-~OPl}A)n61k007}H_1lbYUrp1O9Zs%BD-$-}-!Z+0AA9xMF1GU9z^?+P5NY=U2Pr{YhxT|obU-^cb`YqtWHZ=Gke#@xS{bv_WpgKO)Xnv zT#dEJw#~DwF*5rHnmnsDt8(^_k30Y538+@H6tk9CL{^^)pp+Q9lC?Abs>2lN%VLXl zB(yYUO)S=!W1)3xmP&T4vC7#n3$`bZ3$zPc3Ryl7+4Olb) zBFH*?I`nu#tOx%yu!uL7Ee^740LsQVp|Ob-uy)7#QDD4*JsvCnh1uuR^xA$l6Ho=W zA`7jFpu}uMw#6*On%P4F1+zw$#Q^3hPVy%+8ha?h8fRh=Yy3pUSY4iUW?Rb6Vt^)~ zJWC;Jzw-RkSCc#mu+iyn00E5F>XCOB5RpVEeoV~B-WidiEYnYA^hl7(0jsBQ+}ign<15lf>7V{DlX zv9ZP=OJoeRj4ZCic$~4VagA~MJg4y!)UE)^X>r~02yIX$FXn56?G5|!d2gi4&%LhS zf#__!agZ&pw#I9mqSjcUab#Ab6hy|(jF~ZJU=fX#%A&Kgez(Rc)a6N|$F_Kkv1Ilm zKAb=`GEQgqP|eQ#6UO*B0A-oIiq0;XjmmZas>ZnSlacMjVw|ywHMAKtc0s#p*_N~M z#z~(dvry||mQr?@buF7}*1cJYY@lr=3$VM<1GG9RWi7McfB&~P+7;&ykJt6Cy!?A! z@H=2&Rmu*s&Wrs*J$n*Bt!g&RwwRqxkD0|AABT)7X49FKH~w`}W7o1D9-J}pQzGLD zjctsjv&>THq@ESEI;iY~##f4b0C%;k3(bIV%T_7RrZoW zy?~_G^3$WT7~>u)*_o$UwQN@dPJ6Q|W7FnopC`!18HZZSteHKiY|7cSekW_}LF0@+ ztpTVMvl*u7GEaEDJ^*#q@h2PO2M=MEQr5MsiS@{0M0N*IYnH<0toH)i=#ejWW^81k zR>iE0@szSfmVw2pto8y#Ezok(niZ(2WuaBfme&nzb^@xiQ0w!CIpL@Ob6(4v^71cv z&hNmCKS^j8w39ylvI5J(>}t(UqbJ7L;gcidKs(0x)ES?Q8K;(w%sPHr#q3dK?|y7+ z%ru2HHY&T3?a-pqCkId%V;ywXDl23Kt*}L7Ay%J>pK>$BGR7)qamK4RD_DKxiVyLV z$0_2C)9Gmdl`fAY*2D_e1ugY#m_=qQvZ-bnf9lNQ3ZR6np+#ZeT!}Tn&v`Rle(E*- z4lrmOSA5MHgY3q30w^>4?qSP4O`fF2$SfkO<5`Mq26UTeGUJ5Chhd73d}hiRnY|li z*WFGBW1KFJys^@!q{eHKVkO2TPKEQS2BH5O*Q&O>W{w%Py)gTl-uO57U(-tVAhUZn z;O(71jWeD$4>D`1wbFX{$@$a!f;MaJnBvYE)1Kwfn{8%HhsRwWc-<3Fy-en=MV81| zBCEu>x5*&08K+on9sxVW?5WCn%Gk3M$)8lnCN$Q-VsDd~YWA2JGf&~f7@f`W&g@r> zj6GuP^hwpM(kJIn-@>d;usyw*VwiRO^w;RCWp-(--SNom8!oc?Gv(&K)o;sxxvE*8 zOf1SDtB}=EcQz;GtV9-Q*DS?5!qw0&Vy7<~nPt%(WOrw_#jKcZi|3d=%`^qE@uACO zW@l)Tz!+YcfO7h@x;%x}zBtC1{E2#YIb-ee6j)RiW86J=Tyc#tGOPKgKXv>m&e#ih zbY}l-X6f>H&0S;0O`uZ823dJy0qY&SNsL$gwAwt@+0?R~Gj<(|HFg5^rc--c-uXB3 z`&ST8vI&f7@~F+TkoEWz@smDhV6lV7hb9l;?kn5g)K#97KR|BP+y91rGq~bCXiPmD zksVX4A1$*I*%Y%)jNgedUe-9ZEGj$5CV)ELCL@LQvBH|!)UqFv8T&GsY~cC+CuuAy zyM=bCZ1bmBV`;2qw!{)ZnOfhoC|?|7j4S52m_=sojOC57$K+3$f8v@&d*e}A2B6fJ zMQCUKX(5X(#u?M#L1BTmrEKqPa|~sbMQhj16q6VGUW%EfP-j;1nsWlj8~Q(P*xUSe z{pTXFv&$2e1=`itqrc6F^;=S@(G2i!HP;YiL7kidna288dEY_Q336(LEA7 zW5y1k{`~dV$ML63EfPQpTYFbpT>^&dPL)X40LpQN$q>;kp{l)5~8c9*ByJQ`U%3@vs9s@B+fG2>5srkX9X5!nN> zhXm?m&HcA2Wy5R}s3~Tzb~!7V6|a0fvNLXD7H5nxK56pU7{hEbil1<&=YeDP9Q%|3Yv?+w-0`Omsh@A6ypADj58 zk#TYTg{Plbn*F6(Jo@4&d>UuWkg-+vfNXCc&nnM*lRwd$O=6sx#XHlPUC2slKl-S~ zIIBF=v$}x%u1Z;b9%i2)^6n*qAc7@3_LpitSv_2Yu}k7hP18(*wz z7IkSG^G`uGT^>=J>z_PitmtW3e;Kr-ee68e4T{CA4N%*oIm4c--az*dWW4F-N`_Z=7KY zF{?sWjh-uK#+F&dPrrYt7YbQUkaYspk;PdH=ro0O_6bMMSjO0qvHCnzvUbK)vt0j# z$R;%wv=LdidN#3>8N)2lqOwuiI%79w?TzJ&E3#2q1a{Z5#Kvzhr^o-avnS^t@Eagl z^KT_LrY)-v{b+?nWK+ocm1mI<8)>b+B+1p%AXt= z2U#-XAj=?yj%rz+rFB=8tabLKQ1oTJf7i&E{X2A)#CUvhuvIOq$e7kF$HsV? zJe0F8X8p*3amH=)(3f4zW}1R>RtKH6%=VhaWBNq+RAS34r7Q=9^?_DCd}d^^d2x!_ zBNK1>U;hK%vX_4)%))CSSgSVs#8{ znmjmTO&PbE?UeBzrtn@sd7b&E22eD4=<~1`Af45hdOSMy#fU8NlU^PvG&ZyAGGj%? zhs?Nr9#odr?3P(=^GIiP%OV++WfoWL6J*C5V~i6(eS_Tl`Cqol+K=MPl`-)7y-Xk@0(z zKkb#-B*r3^L;R#-HU;g@pQ5n{ZMWP}*_5(O8LKY~vJQ=B%otxRWJ~PGEXW$#M@IG! z+ZZQ(a{S~qcRI6Ao@D>dnenkP8)(NFyORC$*H+o}WN1byL~Ni9v4ysoaU@nE8=Vcc zJ1@2`?li^sm0B}-AoZ^(OG>`#p+CEOp}NB>Ej8Y`Y^2!TVssMqO+1%oiMwsab(u@tTmQt ziblq0Y>*va?AB~H18j<$7^Aa&#s@lUVo}+zzs4F<&1T3LXpO9i#TPGcT!l5Th8AL7 z(H2_y;*~xnGrnOOi_rcf-m{l~=QhVKXI0GVps+m%ta%=WlWA?8efwDJtmxpqe0IE$M zWHv3HHBNDJRQAH`>;LoPT9fA&2G*MaO^mIvW!B39vw&xv#TvUY+Z}h08M~Gxfl6z3 zZw7b)&&D{m*!Ap3OK0)MC%(9SSv^mSz}UVRYwS|C)vRk->#UjGH=~ZqdYA&4C4cJi zlyO(^Y>hKz?7+BV#`t0m&KPDB7*}R}$zpgNl}#xNvdC1F~6~ zP56W{p2(Q|sgJBYVOFJVjj@Ya*RyW%gjvgMp;aYonXNIFGhR2NN?|z|V_!^>$T-Z> zn4RZ*;EO*WnO&u`4FxwilhfaQUp?0e5+l7nfMNJUaEpZ?}{+wq3!)8aIAIYMU8P1SMhzT07&0 z#&X6gWPK=RXU`p5Y-BC57TH2eqi1jOSY;l(MVM zll{9a;ANNsWHYju$QV<6pD&!TyF6CeEwU3DlNd8*{D@(SJekKUvybhK)0y2nvmrJM zc!{78*)JlpN1bJ$;_C{n3)x1-FslKmCQvrUSYw}vY$9XAr|pf$8b@Y@EF~@Qa!$SH zoBqFE%{%k*t3LmCph7#w*a=jGR{Gj=5LwIYq0QrpHVb%a%%(FdU{^5 z`$y$Z3{%(`i&$=^(5J4F$u5tX#T#cc;IuWX&g=|RENeXRlgFP7t$?*NjxDy%>iVZ@ z?19}vKZ%#uItE87rSF{{ryV^RAU zVs&=>l=-L6#j91;0aPxNvC1}mN|z_o6j9mU+6K3Wy?}38$JQAO*O{eQ2~?`tsb$-g ztW`VZ8ct$A_8h2(9TfCex8k>75-cMvK zkv*#H2fw=G#CUoR4x0 z%c`2ygL!)YlU4SlnC<;f?##|Kg*!copd3K01WHfSs?OfME0vvM*2u;f7ui&^Ys&Zv zTg*`a6=%FuR_=JFj4iU+a`$FHe6f-B&a8$O?TlT?HZlJ4%j1Kgsb)C|p!{U$nOSTt z>(*=|<)`#JAZ%04x{fWej8kL_FCuGdA$CEVK2KXb=&WtA zk(D!k?>%^RW;}OOcxch(Y%Y_L%J%;852dnm|2XjzC&W5`60wP(e#`ouTeD=wBG&0s zjq!e*)+A5@w#<@0^{A9h0<~rqpE!WBEe^8ePXG(E!M4`en*r8YKeKX;Ke;t4WnxsI@N4zSu!Xm#p|OQ_m{s~Tnem=~N-^77R*`WNs4qDkf1;9g0A*XeoH5f+ z$m}j;%PhnSSRMCy#u!hN2Upx`_8_wY*O$rMwDc)O?Q46-Uj9Y;9k9@PU$)5RVhSNU zxT3PVlJzu&YuPczQ_4nVmoI+55(};vW1k|6F?ORT(-bF-Svs>*%(Bn3^CzopH1>D@ zmhC(Dd6vk!oSgtlI%{W4lP5xJWJPV3dD7=e0@d^hjZK@!#KNrGJV}hb8^E0Ll(VtL z+Zab>Q_Pmwv}HxDHUq@$xZ?j|EkH6$B`a9_rMLeLEM*Pt1*O;cJFo*|+B}C+R#Kb! zCx8vJQ_MPkdS@lZa>yjcvp35Kl&2}yQ$GBh**j2s2%kXK=Od2G+1g^5-A-wSkvG3cU)|dgD~H35^YG8$GII`Q!xJDA&z;eU0zfv;SAE&Mcwv zFHQp}Ln~{XRi0L}a>dS|5*T}u!aA$y>8P{m@>przmzB<%*-)E1p!}c@18Zhwjj3d( zmhFvgt!1?u5S85%P-)Fx`C(dn0ngspoml}(Yxe0!29OyG*?%W6=1Z9|vlNuGcw>Oo zu`h1?&|!=vv$STBjfJh;fd#1WYO70EYzCZn+2@?YtJ5}*Yz9F{7KcU z11My+L&h#-34hSP2v%R8^ME059`EtV>zODXOx|7pI(6W}N2iL1TS2g{Pl} zSeUgnj?R9i%>aEZVtbphYFV7IaLtf0w%GAgTRh0@s4Ujl-Z;n>So>nEagK?-i52bZ zddr^mzb9pEoH4{Mm2LVYR+l#hR(9OG=YH(WI(;&=GJYi5kf z`Y|8-G8q};$)9454_odXS~Rmm>{VK`Yz91R0yX)Q0%NW-=D=(7CnL+iq7EvXSqiHx ziE&i6mn@>Q$)7U*WT9N2fBo+;N`FV`8~W#z&Q`euP_U+0CrTy>Sxj z|KPJ`>2-edYi4mOS(USC^VAmW2hUOjTJ+T?;gfVW#IhNX#;ip)k+F(dpjG~)qe@m^ z$)BF`a{;-1 z@f5QY8H?B?P~gg;ptUbnTUM!Ynmpc^b!#^FEP9qg`O^|vx;(p-Wt3tPs1smsV3#L< z#=OqA?K%Iu*yl-?XE9rAoB=4hJh^2i4C%&vW7OywwNunPK=44 zw#rs!uU(BQV*B{JHfGi4agPUUtaHUNurp`;KsUBoWt~8=88ABQr@41!*7HwKo_wy4 z%B-NB9uGQutYwRAjIojJYJhFAtg(;umDIQwkoW%aWsGUf`b1?B*dW_d7H@pRmIAKX zfpg~Q^?mP0VqunImJjoaFP1rW9cyxHjNc`ILS_fp)tSW=^WjknCs6+^k)6aCrByY1 zC*e~gV>f28#t@sEQQPNX|L(3YrkH02NM%#Ys+eV*LLU-YQ7dG*ejI39%Zgc*vJRj` zteLejjyE2e)hq?hc&srRt1l(Sz5ohrHM58_hS{&{j8|yv)c{2H+f=i=G5Z%q#+_ND zl(o?2&Ne}7r5#r+P|;Wywp6q3&R#>|#{AFM_r5*fe-q{Gx|@PxHnx~GcRI5!W_Myt z_T*w#%!XKR2Sj7t=0RvhEQkC_wpgD^j4{U=S^S_kwjBavsVqXvooxbEFTJzv?#JRa zeV)~tC4cHP#niGRv&2sTn_^b^(~?=M?8t1YSp$2RG7hp%jA`_U*)R3UVgSsh&y$te z*y1t9-VA8|^iAR?jIoYh+2)~`6|i0oIIOvsSumB>f~$m<*XZm`EoSv6%lVS@n!a;9 z@kVy1PZ8Qkt%Vj_Y-ijl<0Z1zSq~Yjn005?B6~oV>8Dn+tv1-iybtU$c{Jelc5c6;wNb=DyvGC54mE5mJ?$2YHZ;(=f;TYkNk(e zrg!f7{%g!^$BZSgzpyd3zz(l=#?sj`+r)T{EcQXO>GB}4{{*ebY=$X1O+hh>$Pz!v z8K=P`V+^xaS+Pn0WnC?1~teN#QD`ky8&0YY3@n;i2 znOQosy=T$JIJX(Q%Ttk+#OhdLnWpfG$Y%bDdlpHIvwHnmdRFe3lL4sJZ-k8qpT-bIET4}6% zvP8yKSgY)$Pl&9Uy#*qB@y4$l+{!G}Mq@#CuXZA3&XqBawC4oA0d9LJ(uT;wR&bEi{ z%x0M4k-V|zpDMC$&E}fL5m^VulRl*}>+vU?u{74TEcw$3w8@`ln1c1Y_+l1jWsKLU zF;+2)Gd8aWdvZ2<#I6_N3n`4R9`@TQk&~XZbd-h1Z(%%4h1=z?e&e$r8IgU5pI-6=%&e&%} zwtb#j<6i3tFsV0Ba2m82T)xM*cv;TamI{Atplh&=EKtz7~?#(a$P`9i)WY> zvF^)`#(L46K972`J}zWeE$jWeFCw$*^U#+4&+hV|vZ`kzv#6|hdAu{r1XN1dGJA-O z<%$y-*Bs9T6dfL0V{tphY}&FnS!4M_=7QC${2hqIjx`RlBe9s`sb)Q|=rQ96?Fx-c ztmmI*0LqbZ!Y94b<#C^TAjy2Bk(kBD@SIPJ|!~t2$Y>M36u_(88ZOI zUB(1ZSmTU9tzn9*ffi&1?L#3eX4fpm_~O;(5w#@77-K1|G!~gnHTx%xEIKp(QUg$4 z9z?cWhTMP>IhD?@B= zM$I!Tvw!CVYFD#!0TjT>80YA2)ZDUIk)5GM0c&Pk%35cq%Y)2zna2}Q0&Y>1u67@aM&x%{cbg6tO%JL`Au z@hoO{02N^EjD@VA-N>%h00LuV7M(pc$ANX}>@bU7-QbbMCAY8mJ$t^tJb|%3rk0g8 z4z#<|GuHUvj7g1uL}fFvC}2;zgU;jZ@5K%yGGn!QVvOD9 z@wKR04afps8a=bk<69P~Xu*{enPp_$hP7RI(@26v8#U52$QrRbFc4}GLJW*NcZ0@CS zW_%o3Y%MEf<&5o$?Tq7%MQsKa-I~qPY=8~3<<-u3kR4oWjxRd_F>kHl5ivW>v;=k{PF*%_N1(+1%N-291TRsV%RISrI$S zJl*A4`4iPF##jQIgEcm^wZ(GA^2Xv7YLQvBc{H>rkp`tK8ECt9mfI7xcSYr#V3RylXv0`=;`~BEr6}2~h%y=1tn~lbs{9nI_ zJz=(;9%aU5c1l?%P*~%5;{g_-ol%N4vr?CYFRaTKz7PmY3wd$)8+BFqIlJ3Iy>VvQRa*BFn^N@7vj1#L>%o8Fo=w{P;h{wDvoG5Zv2Od&g^ z>{3}7<1o8?@!G&cW>52{w0Yi7H7i^Z+Cf%{@gX&41CPxBPgEFLy@*+F^L%V?EMyO* zZ11KpvMOh1mI9R(vHFV2s?qaM)$F0m(^?jpMPmV$FOVfR4zq6ZOet$*qp~oYE%%)n ztI0EsSx3f^+2Ivt*Oakav)60cw0I!4d9j(TDZWiAd&!Mo&7TVk*u=%6)@M&ZNoK3F zE@utw|Nh?|rI6AtW;10x%;JluF`Jpi!_Z>N*@VX4z>CVdl#MU$YJk0QRs(X$BIy(P zlOto-vLMT8{zNegu{si24l>&#XN)yIPE(K>KY7A4E2lGCb8K7e)Y#ApSU!+heM~Xy z$aojCXsoPpbe4ivBAbe~k+Edf#(3tR4$OWpmF2_8-i9tu?D4C4&z}9SN@sQ@P;ZUS z(%|8|U6GaACV--t<)E>g<1SCUG5J#?D7SfhHAM!XT+BLv%Kn|V+}q{}vSt=!@9Gm~ zGfTn6s5#=b*4PU@=G7|O#MmQHW%iMf)%X*|tcMmqLujXztSMnQx zEneO@8mm{FvFlliS!8y1d1f`>U1Qr>id-k7Dwd-;V^r1$k<|@Qt!2}h-3b(t@rPxW z$XIzXUqY56U}4rPv%Rz}Dl3^){#0fG_KN*GE#PVX>7ktQg9nz_DQ4$lRERaRU71ZS zi_p%{V$HGovTn`hVOnj>qOu4rDtnBd_ACVoYn5$ZmQf0y$&5wo3ZNi%kaamb(B8=B z-#zP3mRItYz5Jr9F{@hk7k0+>#Zg&=7GS5zGx3vnjnW=uRyU(|0!qZn7=!Kj;bTA0k;UAy zm_2tYSq?Ir{3%QBZuF#>^%O0mG@?_}Q=0Rj#%pU65wt4D}Coo>j#vA|jJD{z`W@^zY%RY|}s77Z6 zCDh(DHlOw1`|{thXa6e}SeLV-vbMxlS<2ZmJJ#4^#_t?d7Ha2d?$}}-zhrB6gqGH9 zuag1TM8-~x1?`8&QHnZa6RX~=7XzZQl(UVWxSIlJO!m}+%CZ1t#>*F1WMhpfX2%&{b!Kq|#unN~f!1A~%s&}f zdtc%Fg;>f|{e-o9HacxJXz8<{n;bN@JEYh#Qz*5PK&c9}A*{Cc%p2?pgv~J6W+EH2Y3boQ$LtBw8uUxYzTWn)oolO8` zWCiVvKdCS404l>2VwPGqdGRF1u4E6zY`Q#9+XcK5`^nWOqq4e9hG7ahpa?W)HlIS=R5y88tVf*yFiH+@3REujG5L!iwE#%eu#trCB>;jPdk&GW`Uz6)TRhtqPn##HvA5js>&O;|TE-St%!*mUCw*y>0%lds z${EkhqD3~W zGmGUl$X;DFG6q{gtAo%kXDnDXvS^h()UvtEnDoiO9xpTY)@+#VgLyP&tWgR=TR>eKb$RIuj(6*IYws9E3mTU z&MSejjj?vzS93PStYlWlz$Sefm3U5zSK<%#(;v+J5gc-2uQ3$mux#NHxT9JBDM z-n!@d>#ol1Ms_U*G%!BpmHY{b<>Usa)UxQTl~&B^NMxh2^2L)t30Nn_tz`3@4~#L> zPfefPmld=+?(+1;Hd;J<*%j+58O!YNu*E?(O`Z|isH|$)JxcLeOIa1ONuNASv1{4J zPwC4#F>XCOPty{vh-_tcoH6N>&bQy{n#CD1o-U6YJt(XX)TWl5_$kQZjH9tqSbyFk zSA1di<^Q=NdEMtNV@qOeYGsVa884A_WIV)1X49DE!O-!>i&)Q6kQl3&)u(D%i|iD$ zDP&FT0og-m_JC~1jAe{d$%5<*K%uZcOJtot4X{+QGsmIMG0^V&$Bk^`r`)o5n5Edv z+8L8SaV9nP{@vG7S&Z>KGavy}Gh-p^(3rTGPfA(>V;`Wc$QD^q%OpkTpO(&=)>~(c zCAGZIF6U+Z4rE}_ZCQ%hsb+&Jw6>Tvvmo0?Lyt9PnBu@J#GXXPomfmQ3$PID2Sdvn z_x>l@Vjme}Gs`H2c6msj0xhcn*$hZAJ8hmuPyjnCvo^*+n{kRT>t&ubOyMpMUDS z?W6W(yjw57hJFX|#4}4_Vkd;!<*X%k`C^#0H*Pt5;C0LFcE%L6GfQEO1zH;c|Y)2NSEvqNsp|d@9#!n_OhS)Do zH5O#8vs29iZHw97W-MAc-oGoaR$0ABVaFT0F>71=KmUU-9%9?)nTqx{jX$lr{KDHy z`yGJU$gB!k%dFF4Qe#xsGAmO&8+Zi9>B|y7WzBsu<1xmPT264q8edC{S24@Z>_;Ez zQ%?*Cu?L+^{3M~>ml@-XQ_E7y>ckk!9H*9*$YPBpv)dUbfJ!YZZ#G{}!u{9QNoExCvm9OI{1;Z3q`bv|>E3;G0y3M0C_Z*P@tko>knpml9 zMk&JV^~;Ra<8d)-W=}R|qq6eEtIcC#w>1{AXl-s;l*WE%on0bJL7QV=tPYPeW4(|V z>*Mwr<4ZmJGXJEZ&GWy2Rk&Ja!|SMQpj{)2iHudu8reKb%hOMyHJ@tBav*lr?-CeK z{4~YvG0LAzd)E=J~ zAlojFidj{%d=VIPx=?H5ayFi-WA2JkFnn*CbGf z^hvNvW^a+U9>0~B`KCSNU(0RT6tyAN85FY=NsVibCA5BKfNw?R`lsphlvx+DV~pPy zv|4hHF&4I*<&9597JZvcH5O;Q1E>mZkX3JXMaEUxY~N*MQ98RLV{9?Uz*5U1wEHfZ zI%C(esb(dyJ|0;#u`7Wpva6OQGu~~U5Sw8NMk#EKn;6>|7uvt(FiPQRMIxxqK&{AF zoX)w0^H0O=8FTiMzx8d6e-URqjaic8I^(EpvnPykpjG^o%y@@S?=5GH%yyo_Ba2bl zOjGbUt(`t;XO;u8`t;-6?_!Q~n~cYdGqQME%*qnG%Y)4Jnnf4064^4V1j_d;ax-eJ zv3+sJDF~pRJkh7lQdzZm92oP$VgS1VoYj}5lqG;7gIe)Zi&>YlYcrsg?24ZTS*fh3 z)iVQzS@&iIEuOe9^FF^#KfdHQ?s@)lam54eGRC;#oK&(F*~6u6L{B($XQCZDWKn=~2HL=Q!Y0bKrO$1ey{l+?*#8^r@sj->G8Cz*f zY-Cp0uBXfr88cAf<8t;kfqM8}&J(hi{#~cd)8_0hW=n0RjC})?k?jKmYK+Y+ot~&{ zW+~*0QCbzV?{i<9eKFQ}A?pB&1S-&eq}-UN%!*n&V@*^1MxEL8d7#!iv-QRxyE?O} zX0JR>L1^sxr#NGmvY%?qcvMz0`#9CCB86_Lr@W%Jii`=2 zQ_I>GD>Jq)KBz3d*!fdu79AUtJ{?wOsbzIS>~_W-fHJXLXT@vH@oLSQSpujMdrt?E z4YO(Tx^B@+87^v zaoRi{fEr@$jXh3bnUyKF&c+uffbwoYfc@Zfn!?Wvkihbh2nv;@mUS^3V&jW@HN|h$ z<^kB}1jamB@`J}P7>W$lbz%<5)Tm$F-B)9BFz)Izo!cn*vWt%WwrJQT7mX3^M) z?D%5KtoQE#wo;q;Y4>G2YCO((uBKSi6zcN?+1u=jV;5fjJNW#5MKP;y8hzF0Vs@(8 zx9f~iTFI%J@+tAoaxSa0*#7{?cjSf7Emi7|a1XU3C0No7&m)UrbMiP9&$F)|yGt5rR+gv*$luI z>rl+jC5wuUF~+`_qQxxUxYn31kNZ4HjB&nl4S3I#^8yOo}m$L5jtVs&Yv0bs9amrZQvd)c(8BYW9;RAzg-JB z4pX49&Y#-l@t83+ZHFl;v#0Y)v38b9ceaTDWctmhVGjb({Bsbs~hi&WOmoGGfY8e z_A57fCW2zhxa)V-S^HwS<8I8lo?VeKyv7-iHQxDCc;%JI7gpfJZAlIKvca^kkf3?pd@oPM2qfDSq`U+2R;uPeA25V=1gPb|p|Wd8W|= zua?m^HI|`>yj(Y0MHqegFOH%QCUZx#;Xm zd2ip=U&OuH*kV~@4okBZ*@H7Ku%~SvrB6dFDO5YNxZ{Mz(%4A?BHPH=wQPhIjn$`nJY$WWKOKop zV!Sp3;*BM=!PT}nGONJYk#UioL5hUNh^-`ck5kAQXPQERu>&YEJI6YE>$^R-m8IA4 zebJWnK2Nk(q^6clHM=6H_GN90r_Vzf>q1u0y2-=z69Lpzvt60>*kW6=R@!5qCz%ip9*ZPu`5}SO)dLa(-hqQ^f-xeirFwb4|7ifrPTmF z$c+2Q%1%>=R(s?9%m8f$6xvAasmdyV>I4+Ts+84>hMy7{yT{W3sL6~C?nP%`!#n>5 z|0~356Jtp%0~Nn;`lJ`KC+b+)<2HFRXN=GqSG9SjHS1bdyh1G!i^}ffn^Avt=T6MA zhbg47ZO*!y#TV}>V@*IYO7U?-)22X22Ix*4k=W_4H>=2752H2Fb zEoRZ#sH|reY4Wtg<2H}fmMLR8Je;VkfR)N}T*%Vru`yo6GE9NUW|%_N?1#Ou&7B?* z>-@>S7;H02v3j%QPdqWeGE4ZRPa*4Sc5X91YzE+qJ7+9p#cYp&%}FK8Fa;l!vNFa! zB8*K z#nt~mn3cvJ+dQV00BR~(>~WsUBavNu?(fR=^TKDShH&M0WZ-Ks$}uw0IV_l38(^#_V6IWhZ}PjVDbW;To-V zW_^#an@sp}qj!W4F#-5}| zCCdO*0w`1#TkJz68;zwi8(_^W^(-=*S&DmDWBK9}WKr4_vKcde_yA|D2`D0CM3&>u ztV`K=<5sg18Pl32Fy=5#F)GWNdmarMu{qel9``O>UYgwzTqo;uujg8J;2X+$U%s-fo;@i1$Vtuapi1hF$?OlMZK3Rih!DXlxRii{sT z7?l;Xqp?PI#ZP{g)^ueNS{HWTmkm&zLdZxFd@}7Hrd& z4YaLg8KnrZ9e|R|mfFle(U`R__Wk3A_KCc40^>|9Mr7rVA=ZuA8e>c`0hEr3MQ2gi zLz8EX8F!l}<4>a1*e+@bj4iUOn#C07vlENwVU2${YxI8ZH&2VF0aSZD^29pPSOVjt zGxm(Jz>O^ywWq~wJG1Yt^a*!NE!*ec-Jvlnl_fKF00prhQp<8^%SL5kR!1tkM;52a zlZnM~#t@6dN@JJIK8(zIHvnQ|j)|UV^0+m-W){sW8tW0Lry^sOvQSGoYhaf(c4lm5 zeQDdb5?MRrtjn%#o-o@{ig;sBEl&L8-mG}V9fK{t*tzjVXMgS+`u6|-q^uJeA4HaF zHt~~~?HQdNl~oWqV|%h^3l!8FAXyHvK$ zxQ*Fy#vnV@tavrDo-^LWQqCf=r&Gqxj7=3t#NdAN?8&p zLgP4NTjNM?MQxIe);&7!w&&&Aug>&6fkHA{$`a zn5D681i@QOL^%%aCBCNy>c1+((S7~_mT?Q0f2|AaH{)Z*PTE1{*7 ztUF~*LJ=PsEL zJIqqf8d_Q71W;Dl&Qf?>5p%5A7+Xwctk;Q)8$s0-z=G}VjftQ7&VI&w_N@Oe zi0oEb5o>OvvI|)WtvB5JB5XkxVnKG*v#w;5K+)w%n+IF$Vz$#1fp${k)#X8EGXZrh zXI0F0%6Jj0_({AjWUk!9`bcAAje%BQouo+gMN#cVpW8GjP2zsiz30aTo^ zTRbXd6&bsn^)v;(xWz1yafk)kLz}0}rZM}wQP~w4KQOe`*%Y&4w)a0h&f#JTZl-Yj zRFQ3HtPrXJl#wNT(s3=@&Mf(ph4$KA#^1D<&Ak-HHm%v+5Lk%S7e^w?k-}PJPxGe~vkV#sTdXlMOJ*FIt;W_G z>tev*sOJ?`TVuUs_>k+8$Qp-AiA~e>grzuX9 zmJW|I?ySGxR_nec$nSRm@(s(KE)Ti zGds{)Ws9v=1H>#k>o!l2mCUxyV`scV;~L{$XZ%U3S#Z@;W~pU^ESWJM%Ab%~pCH?E zc2;>Tvnphd15k3t&7Ul?mRa{^+nR+~ALmbrjZdms>e)4A?1C0+46uI%*F?rF2dvC^ zG!|sfsFXFtXGz}A{if69X*D~xI0`%S6aWjg%Nj>#MQor=U-oE?g{@UKI%|>j{L=^a z#u#H7J#NiLXe+TW3$l4;fPHbGJ&BA-pY+K(&x}B&Gb?K>U^x%77+`3%$g`PsV(bXY z#cZCS?GXf=IPt0+MRW(Zj)#u+WW2{Dxd1aIWW8C>CF{?A- z6U^qB0V-zK%B-m+e@X^rolR#p_3Y$N=2gx(ty!S8%mOUT?#$T4R%Ssqp>Zy4qnagr z(s5#}$he)^K%1Gx+2&C-`*l0B4xk`*%^6o@MeMhvPZL2AKq-An0ENm{VJ9WsT?hr!;0Ke}YvnE!|I)I{>jWI@LrLg0S1*=bWcHNAsg7$vKDaIRj07~Atbu1c-$ac#3 z(??QRBI5(HDrTu>?TZ^hmDpU^#+dP8Goa}cp|Jy~Fk4q_orT*@EGjaN(k^BrvZF`Kx03efzJJg1|3@{uWENz9fk9p(3$){l-RGGED$clloaR*RHteA~09+mZ7G7X@b8NWj(9`O_RKZRKhK)K69pNGV_omm&N3t24TZr`mD}MWp#pV zhZYU3L*r@lJb_t^F$ZVtPLI1hUFV54mdwT&2U&_)$!t2a!qw3BVhVr_v317OvwLNB z%~I@vMIpPXy)gUtyo1m0pDnW~XQ7od#Qw4(i!nY1P|QCWS;?&DDH0jC#ZzR5*?*4A z-kC;^tJ%g+K%35Nbk>ovTeA?G@u$7$?vcgLKXq!6Vm3NkbNmpM^~DtG^C&Y;{uE$8 za{vXfI%>=wW+_5!+B^=RVAe9*X9lD(8=1|`6hanj{0(PRc283*Y&}nra<)Q?F(!dJ zM#kfdY0I9W%3gH#r~k=gW6XBIFpDn++8nW}ztHQ1PYsMq>`XxQi2-fQ&Hz-raWZ4` z`hf!|M^GL!?v$~M*%Y))KpmeMU~1<&8C2HJm}1s>v4It{sO-qBk@dvlr$%;UR=g&E zGOu)I1c`~(#H?BK&{xr;5X`LB&F@UuIzJ#kjz7wx|`Uzfp zo*Jb%yDm>swV(c;J@$Bd)1w0Tx$oC~08jO~ouogHi3 z9*?Z?)58?2n$^`57THv@c{0yo0BVmJJ28&VUf-Do+DG~%fa1zF>8y|yw1+w4qs+z| zJ2LhJltp&UQaFAxvKv?o@)|NmXeF_|WHAePF~>b6G1&;7p6 zoy4E^{Sz4LjK&f@MP+B2Vmag5;xz#!L%fMKw0Pqdv&o+_v?yfJ*&u6~b!6;~} zJhRG!%IY(h$w*}dtwt6tw9cPAXndTd5U^%;fJI}OfO0Xr6JtALk1VE`4Y1CiP+48H zIGx!qzf^Cw%x28kjUL7+#OpT33t9VON?E=V84KF*Dqvxjsl|wF=bxmo!uJ2uF3%QO zY%z)P`K88}diEzG>n2Z;l_4HpA$F+Uwd~<))REZ}Wc8E}l=gketR@!InMG#V3*bQP zw#J>Mpw%aP#?FkXWgQx4{E6(T#(3?_HZqPgPGXG4l0adMMXaHn zdlt2VH<2;LtODai#$SMJrztvTEMECEw76mtV^353mlQT4YhWEcS!G+yBD3&nVjVtV zi)r%AFa_9FWu>!xp5yt+@7c>gxUwg7*3^nuh*kKc07|ti<4;`-$ZmkD*`vx%02P_F z$cEWoCu3p*%qfUJH&d;c&hEzXp&i3Qpi<1)7)LjT4?vp6j=(` zBI`<4#+U?ZoN@9eMApoH53zhAv=-R&RN0F+ej%}ts((l#Yio?y#vDsvd!(>Eky-O9 zW>Hz+vY0)0sqFu=(kG0uM=7AT7gKm;Rx`)p;al-R6-oer#VXk&Q9_BTuYcjUGx_*Rq~igjgYaTFeqb zIWW$QG1l0~x-skK?4DRm_~dyCA&bl!*m&dS#a;}kGag?Iuu|G{bjA6ef5LnA@{fes z;A&GWYAIyf=)oK7F&|c0{V zF$=E?*-4))veMavPavyeb`hIxo-MO(^Q%%sWvL_DEGD<-)tNEvc$~rMNuWj+or?-Q{eY@m|04Zh(1R zz_!OT-dG~L1}RpT=Z-dJWsPyh6tkAuOf1S89|jgDGR7C9v}=~)x2L;29)Kb*cKj4- zdH&r)WyVUMT+SY9StC2Pc=9J^7FEmYkUzP}BVxk^6#YpUG@ZgH)xSDM- z`>rb4Y!c&W ztf9pelNVcQ+vUL)3t4^AnAMlhR#{Txv*Yym4gWoF+;jbB8$VfPE3#|;smOB6VrD6t z7sD*aw;5B*o(QdxwbEw#iPZplv$Dq0SftjE`N)*9kQKCRV-{D;iN+>=N^E>@WHt%Z z3{zYse=@W&##FOu^L%P|tjHK(Ka(%U80*W|KRJL(W40vrA;-iw$h|?*uH!ddhfiS(MN|p)*@!yn(IG+8U>ptur>Uomr%sol;iT zSlFhJUCd@-Hi2=mb^7Ew7HUK63{uDyM`F)gWiJ`j3(198J7c?J6B~1Edn{Z@jAxgp z0~Mn7#280oJ^b`fLe{>xi7{7GtWgSEW0$hW&TO19o!J?tpvhyQ-J!AbClRZYCJ)mT ztCl@HOiRNQDrnu}8I6t1K7K50JN#s49BT}*y~|kD?94x@(GzRT69fM9Kc%oI);Ix_ z%(+-D4~8CZTDacH9DEDSv%y$27rB6FEcAIC-QhaTd?K3NdtOKZ6;}Cl~0u^Umjdcb^`b1~8 z0o0FV#^;_GUv%~be;P$&Tg+Nw3vI?IglmQ=Xz?Jk3)&((-q-|3H-Qqeel+y)F|(AiI(}JQ9_3F9 zS$kucebUoe3i`6`&C=xAZJy*$Cv6_`Cn2j(d~vLCrk|#otv7ZlyF0Vh*a3EIv7GT5 zqd0qR<)VeCy7ho>JNRV!7ft<5**>td-Wj_!_f7JNXmL+7-tbYh~7bSz5DISp!>U zV~i!VGR9u!p*NezIL( zF`LG0TC+sPXe<*@%u^h$Y?IWcHM_QX2H6qWj6bc$Y}2QMHFjJ)A(WFR)E0qFtLN;A zz4UsX4X*3il(A@R;wNlzl~yIJ54`Hom({14buAm6&6II6s5E9*DVy}E$Zlq%vW}me z8MntHWW}qQC4D-XSsawh_Wr-w8XH;9Qhc&P<7ljr6|tX=Eyfui+B{O)B71GhSjct)iZ+jKrid>lG0rki zfgOGWI*TK$oA)?_~Sn@?c8gtpK| zW^u-%*2dWJQx*0gvrwD;JD3%g5d8W(b&MdPOi&k7rl2G_IP?TMVdUV5t)qvB;SjdW4Q!AaFF=L0u+ZVSli_ETPR^Hn}>!-|SXwk%4 zXHQl3(U#d5V?=g_7RziyV@d2DfQrhd&jYqgW@U^M87F{ZW!8Z)rR)xjJx(!Wi&M{b zo?>CUFEhp+i`Tr`5*OTa2lk?~&-{ZaGP{TcS~l>e(-V!=YmtpNrppthRd4ph7`vdw z8i&~fviM?suFEM_V!SeARkAK-e*>?IjftPulX<-6zN~Q{Gux>}t+;PyNuP91objQU zMP#+j!(%=U&e$sJ&=_K=WzDR0*3OubMV+s|T2~pn$&)rukr`AKuiHzlo$)EI9k@fnWbXI5PpNOBPmUS^JV@#vR63Z|J z*oxRJ2G|?Bn)UQks#yW+BUbmhz`A}1p7EYN@4xM7ifHT}TEraN6;sJNfSQ|8_XJd| zF*OFKryuF{kt-&#MsbAX0LZrv(zeD=K^Bocs_a5G>vsyE ze3`M3g<7j@?;mFZDk6IbjURw)rYRnA8bNiMV&9Cqc6rQfbynPdF}8S*Kef$cXS_x! zHncEXWIMCyaf)@B%wMInoaV(VG2U$+feNuX7h}Btl=|^^?HT`F4=nQiPc6$ph4i(x z>~wi{DXVK18K!vG#jJifxw5UiZfiWf*~~wE;8J!}Rzgc-mT?LrTV~0O?Tqb<5n4p{ z#2L#LTV-@&MYglKCWf~R-dD?w#Mztidk#ygvP|ib2Wv4 z?Us8pW8oTLVRlbbRAik$4Y4aTo|#1ntz~va#vq$U&wkUM`CpweozM8z+v3p=sH`>C zI@CpWs7Bu-J5k+7Lk=L=0zvKX8wuH_>Pd(%U%u0WilVt7>ilr zr+*Q!g7z-R>d`!%rSLq(Z*7d-=2`L6@2=Rw6R<*7W5&P773)Z4AAb5sAD?EKXU#uN zF{?h0kcC+t#-9`!dz3=i6FQ4CZur#3Y{!h%nC;D|D}NejAy(m&Xzk1*kR75K|Or?&v@gW``<=nh3s34*@ni1PxxY&vZTho%s9+qkHah* zc&cdId^&_(^3f+%|q{IqO-9=&bUml(N3F%_spr9EGny8 z+ZsSEYE#L=t#A#p3_zLOrcaHaYK;>bAKtWQ{+H(t=hNT+XAg>1D>$ zvLMT`%6e>Z7VzM;BAZe+p>c>cvl?0awP-!uvpB;Pl3Kj+UCZqF;u>SC>^&RfwHW}k zsBC1m@lz|=PhHB2ST%V(vsjIN%w5JD=TFvI65|F?tD03}{G}6Pn4Q~XzWPct6pTNe zh-?z$s;p@30F)$_=&3p@V&N8v4YD=H-+vFRj-afv7i)a_ht$*W*)#q-&Wk<$G}Wwi z*37D&4X-ep_{lQc^ob^q57cUy!a6(8zZ+)XCxA-+B&9tdD`3lPGULWicE+;BARB8; zDZA&4?@MTPT+7awv4}-#-I+~ZO#aj!PoN!OKT|PFA*+*n7S)+em#2|2(CP-L8so%I zy8Y=t4XkC>&Dr*5)8t8?N0IS2j-GJF((fqV*H;iZ7lfk8ssF(b?7HA%B`n+pMxe zR?K#gV)uDe(Bh2+Yuxb~GyeMPuUyQcvNpzCKyGMdi{p$Lr4Y3-#v`*qb`}FBe(Dua zvBxGh)SfS#&-l)N%1Y=TAJd zQW|Sy!z|8NBC9jTcw1wDHM87iER{8}5m}8=tS(O~*%_xeL3S!xUrn(R<8);Kwj#UY zCtKt2n#}mJfp6qlX)R(KKrNL`V$5?s$e-58Vno)=u4h(u{7J(U56x_VtuLm} z~v-=vQ)F(3mBaxH7>ILf!uhA zjX4gs7f*b`>imx%f9Ia(-%XdtGHYOeA#W^T`I0Z@Bs8X)O)a~oDFp3Y|74NvB!zfA z5Ie;UqRxwMTr$P&_jf`={`hwZCdHR%Ddt*0dQQ7J9 zC^JT7eF2nJmRgo-Rx7hBGnO$1THo2$`6tz~9Pw&zEMTp(E@QvWE{~Mft32YC77s@% z+t9eC*d3l;CWA3{Uc9iC&}zw@4?dSGrkK67W(U;6kLO9}bG~adR)<2?FYp>>nOL+p z&W&xTy)7%|DMQ6=y>RC@yEM(p2k;tae!~9dJ zbt#)#_G-^R?OJw0dmwfaC~DbQW1O*0#*97wG{A=07~|t*GR=%Fw3M=N8)NK7kBeD` zDcHaJZzab5L}wSVY0tVd>)z~Q*2qHb<(&QwJm=keZa+1tv4JhNOJv_}CEGra*Li~M zJ3*Efk3*>0z|$aw&igr@rfB?x%-&J_G%A~F7Gh`r&b4ftJZ09_Sf1D_JH(1tkgYQo zu_Lm$W7<4PjPb@&Sx#maA3wIrCN$o}QqKDR@eWg1WLwKReX23;GXpBK4WK4{3b7r3 zB7BO}=9)#186SC#D~>TPu_Lq?V?Iycub2OI+3Pv}!ITKf<4=`Xi>ysCFU5Av8K=!t zW~Y{IIZH8HUo2`tw$l{8P6l3esAfB5%wuLX|74lH78!d2%8@aJY_A`eEmmW8RJPL; z(OH+X@XALsV`R1yi+}iokc~4ov}SfCP&USijML?bF9zAkpLS=~2^7|tHczc_W+_Hw z)9CT+6UD5~XsmBWjm8#PGuv{uuwH=uu|K1Z@7Z(yYj=6_oDbEq#{nqQ3b3QJ)>vN9 zSs7!!L}8KH?DMoSd!n*-#@=#AW?RYb$XFLsw4B8lCo}e#aeOh29*b<^CwF-wvP)*` zjHfdzUO_e@Yn>&1+W8X!RCyg@Rn278;*_(i%Y(-1>)4q+Fss(=K#MQ- z0-kv-vz~rZ`lM-!p_WS4XIbNxvKMTh!uFgwJ^kLNEo)*6Exe+&s$}`}8D#5>NsK3e z!WO$R+dhwkR!vz{mR%lO!S=X|eS8Q9BT6W4= zY_W-@kd4x|&ojsZE#{b7)(O=2-^&<}H_kl81=**N{o(X|d&d83kbP^}Vkb{dj7guw z>xOn!vaV&Plr^#}-|0*(3$upSLyL}|tg@rC`z{%%)zQr2ZxC8qvpaukhX-q%GsLDd z>y=rmS&M9uT`DVR<%~ZSuK2_nx6vbF#jHfu!xS6YI%Al1V|FFR7TV@d znWexRhuK}srZp>>)j?_PjZs<8QfSP0O;bo?4=U?b9x^B!$ zvDbOtvC7&QJ27Tvk%PwiNMV;V_C1R$GOjV+<*blJXYGtPu@YJtV|(KkvyP1E&FZ8# ztNiJAtObBt^1ptZ;*fr>E(S)kSUlfDk2v2Z1T3bUD|7-O8k*z-@CS(GVGB|AEsHcy71@;Zre z0;tSVtR~McXU7?P|1P1iP%X3&dpXbg9T=fG;~#OvA@?naB{G&hCNFjXRheaxC&(IF z+B`Yw^LPg8V2s~?&&Jry?!cG~N(HTNY(r%$v&U(Q#7|pgqqA|wFdL1vG2Z!;eX*#m z&OWrxf-DD-t<2KtL1bynYG(0|$)C#W0ohDb9BSFjEJAFVRc5RNDi4NUO6vhA^?8~= z{pBydmV$b=`IC*Y%30gu_+sJOQR6MNUUI)+dtvt3e<}fLkEpFN4zD=lgvP_Gg;vW-G8KE3@y6&L)0Zn*l^nh%C6~YE-39SmSu(JEX=gX7>nGfc0j8+C0~~Je0GC zCqv&yX^rgRkpcF`ZS%O6wK2{(MatQkrck3Nz)ECoj=O$$G{%VRa>hi)UUPRXE0L|x z_BI)??Jz}`c_x0EVm3pIB{mmRO!Sn<*oy%G%f~*?c)wo$_NAVsiq)S^OJ)x`>;CL7 zZH*loqp&*MvzT-6#?{!l0cw=i(-eqo8nY5vbao@_M`?MULYZ-7)&Ufa*{ZBNvrHLV zWrM7paps>6Dmw|(gNH(v%Zwk5F-B?W@`zYe7NIS(voh;i_DScTAa;)#uK>!%c>1!m zW_3EYC|)~C5nDWw@vQP#Y4OJ%r@$Fcb2h|QWntEd@ufBUyg!@HNKK#dtwv)*&^!zYJfeF*|EhE*~qMfwr!sH;>awe?19+17gg36WRKR^%+llGi(*!NS+{xa zBeQgQaxsM(JqnB|XSo3C!CXL&#LfWJ(Hh$rEU*SKT7T`#uy!t66}?cc67ZukVO{#?w; z8oQcpX#BLwo{kyE8rK+8&N512p$)U7#%l7AKhflwS{7!HT^`r6y@7n@pQvRcvVqoX z?gAEMR{$lEMP@%eHhFd}TagX1K#MKznDL>@<2FxbDR9S|SP5;2RV{l6jZ@9GlZCYz8=ha`*(ZY0Nq^rkK4Zv!5WcR@oU@ ze1J7hqlW;>I_oA+L*rz|GqZRYGd8k>PaH{XRF--cq3!vayx7jz%+lY{QDS^B$8()A zD$BtY>*QwCq{gmgnSUCcEwYzMis#XqpL^!J`7D0Iz;0v%?4nhrtPT8 zOi^##9#5tzC}SC?z!}eFGOlJ9vXWSvV^kKA{XcQVtCsy2-Tu_h?8i7`mIK`9u`|w{ zZJvK}HS6r@%Feh#>n6`~#*;s(n4N0Yi80QYCkFUfWhJz_%h>g7fE{N%`O~48ooNb; zv8NV=tbt{g!qXJB#uGnnYaC}hiSeXQF~&%2tJ#arJ{R1xK=aw(q>5R7(n8zRm^#+( zc*($T4eWGgl|M1HD3wiSJhv?B0w^PEW2|!a&Yi=`Y*bb|0Ti=w#ym4% zHF**l`(6rFvbnPDejdz|o!Kq21jdnAtL&pk>;+_&0%CJ#8_rmXu{Q%C_K9V7mS+D$ zfwA)^Ib%P}9f@_D$2u#Cbp(YsjxpApu`Z^N##UrYtiAC(nMW$C&nCuRzw-#xu4ElR z{jJW}?)YMjFU&rJS?X8=>&JWu+2|~<;Z-7=_^CRp0jNe$V5?z@E(hR^_nmD<7Nu2> zhbGTTjVFNe{8Q!?ZHz(oq?o+RP>zS1fpva6_zaufm7Eb^rW_6Ml^J-x2jJ+^xk@d|K z-kCkwzmqNAn*k1u5!w1;aXQ2_p&u~=gi)@@mpvVP15&kPu7WsME& zFl%HPrTDL^ET)*uSj^IyEwI*DRMv+drp+Ti9A?)rg)3S6NIW&Ft#P@z`98d-%;amIQ6onS4p#82_Y z_+qTF)2AStTK3*tW=xw$t2|!7`$W}j9`j*f1ue!{omqW!VzE#8kkATQ&p)Nf^Y8J+ zhsd~19x;nE<`a=k{3Kf(Y$t!R$jTYZ80#a)<4-eZ{2jn1fEr^Av0!UnYmOzc)U$u1 zlx;zKVfMo8j|0o!bsZ~K!8OD#Uo2e7pIpn%FvWCvtg+VFUb1)u7M=Ahg>h~Av_&>U z#)^w|#H^02v4DkG@d~kW#z~(fvb1J7_eqSKKH-cNKj~1-W@q-$nN4I2v)-7EGqyKY zAxkxDnQi{0JKN%mBed{}Gd8p!%X2<@WQ(^m#u#(Q;q?WxKRvaAMV{%MF6tmf6W*w$Do8)_$ig4rl+nYAtU z(rk*^Fl%T*b|hBR8d>UD0%JSlKFz(v&cn1IR?KRWLN9A$wvE|OhuBd2`0+y4#7;4b zG0t_yCKh)Lv_3OTfzJL}#yH-%Dr;bktf1BD^iw3(o9-ZMr46*jwjCZOjMp%QPz77l zYHBZ7zTkI2VspNy<*Z=!Dvy3tDeL?RjorT3Ba23MH+gz*8{m#HR+q=cEGx6w3m|`@ zm>pxhR%X2!5TRYn=Js(^Hb=;&nzhcNvLm!H#vqH#0_=pws$+E?R3*CJdTW$8F!b5#;m}dVTzI2j8mkT#S$xfO8g{S zJTmKX#mh9s3vl&MpC_;?WStq;8`m8lO4)^MMYehIyYI>t?yCMSanht;%MX$JH#hSnhbqY>AC6#u_8D1r}4x*KuXm-q-_B;@QGtL#S~VT}*>Kcz1lot?y3o!N{TBeP`2^m!UTA+s7-e*}t0PTz!FP+61_xzN% z>*b%(@4yS*)wlMi+v5RNR8}v!6*YaHw_}Xg{F8|_vz1vB>#HdUjHjBF(9-B}H5+Qz zn6X4wLYtc@C}u@$ko7Qy1E?@7U+e%XTka)R0Tjfh$1?$xt+9c%Glp4^<=~BR#?6c| z#*5fBW4xU48h?^4=1|HeGoGhup|mPx)tlv-MXd3VzSiJvBcLSFZt6?`ZMX)LixOJj3MKxW%Z+w<#_z5y;)0aMAm(t=1*zMo@~sznAHVP z(%2Z|wt4>9vlK*7n_iR_Tr3x60$!$@2!1Xe~z}S&J?o-HZLajZA4a|ef*t6-@>%6f;eMJHBgdQQ6U1`C_bb?|%}qij23+T4yJJ60+j8 z#HO14Ouo3*c*#TgSCugJJ6>-ecUYiGQx+4~{Z)hx*3i?4{En5H~kokmZX-SN|RLiITR zB$34)&z!MK+0~p?C41rZC-KTZbbibV(_PALX5HmcJsX*oH%>Y0+sE7Ikt^n$ba|q( z)>#L}DD5KFjUEE1cw;kbWL3-RP|C7@m+d>L?3UT(j8WM-&u9;x&nJ0;s>n8Z$}J zX^Pnl=s*RAc*$(us+aufCnI}~%pTGwRMxeuH5P2unsqHZ#u#gCoi(nzlAUcHm_=pP znO#>?L}&5F6thIew0Y8+ZThs3U1N()pt|Pn^*a@_3t8Q=NHJ@fMQL?Tidh@uM=oWv z8?aVWQ*&KJF+NbYm9NmfIG%G&R83>5!x)^dHgBH zn0hw07@f5{4zkGXG-kDdM`w13t?5Gf2HsPh zSx-QTS&6Je;}~N$@OCZR(Ad`4uJ|kYVq0UNO)={TN&|~!mX>VWvfMqM$e40gyh5x# zJ4oT)>_T>@PZwMK(`n5=`Qz{H+x&NjSAHz90@V{0Z%_IpSWWCa{!ZW$8$+xzV=ePk zX2YyRcDH%58sGpbLYrb1mE|g#Rn0DEoLaWdm||9ECKi=I*&27tJv!TBHZqGZ#u-Ov zA91o5uyvMsidM6e86&g3OU5@-sFtP8(*z1;r^|ycZU7Z%S($AuJ5!6x7!R`Y#zxll ztVby*WjSKjr}+~i>st0AvwsJ;x0%kTzqia%lv-(QMHW*$^=x%E^AxD8r1o9%rxDu4 ztM5exSH5&o%9`2Lc&y0S7m$+~@0rEVc>o?~E=EOXoj$pm&CL|!jd8}FF+Rww=P8mv*%|M|*a?() zHL-`QDMGBbc?R0QQPZ*);6nC-?B9Xx4^O|ZZ}HzDeA2g9W<@N_`X=L)vesDyw!YZL zSjb9ZbEYwCZnNfIWHV=+$T-)@+*#JR!xXt0)y~+16vqVW?p^XHOYBnFUCh#)O^@f3 z3{%7yuQ6j^X8dW3*@MiwE!#@A&^{5eI%akaK#AF*mW^3zS;|@QYGdsAr(OVMU=i98 zS-i2g?`n&`TOw{uE$?ESmw1j7{t@F?Ka;Vv$*WxtQ&c zv4}OaGie-TyMiZET$ydFXU2?YWYG!>uP7|YUPSiq#P$}e>NCEz?TjU|99OfvP|aFl z9X>GyMF4eOWqD>m%Go5w*y0%DQCZC_vdyD&fR@{2U{=(Q%ARfp9ETQbjaQdvWybE# z;*7J+6KCuuk5#reK*bu1S0n42DKfFR^CxSpYS}*>p7N1#ie-x}v>sX9Gm9=}Co*2U z0fJVUaT`6ZWIF)`uj_7#Rm~!?r{4JQeAAx!KmLq2_U-%Im}0E4k+sH-%+6CQ1FT-@ z%;vx>##o=upLY0!&~9caW$Ezv>Sq%`cqBaV9k=eih51`uz=QF+|(aO(M zWtTHfV>ViQC}%CQmf1i{VBBI>wALD{$&>gg(-dW2r3WSf*HCgvR#9;uWd& z%%YZghFKM}zD{PGv8&n4Ec$BHOhAcQUD>wUJP)DPGTYW{tg(>Qr$#Bff=4Cmz}U#9 zH4C&>S(au!{ZweBvTe<3X0iKsJAWErDQDZ6UF&xqS;Q9eRbpMsww_(ZEGmmJwkN(A z;|sITAl82yY$LOJU7<0FF|HV6tS`0#EVKJ*?&xfuHMr z%if#6NtzwkdHUxqiWDh|-6R2F$AB0BLtrpyh>$P_KxrNkmLQw5D3dl8ilqElzgPmO zM>GY_O!xS_=S19lv$}hFmR_o=<5uRESxeUvoo}9q6LG)P^poq^V`qR=wo2J^01BBs zt66ZB$|AIJpQvSB%)a?uPO`X=ZJpK3;womnXZ9_v%u?);#pK0hK;apu%VT7rwoZ$> zn8ki#xicGVZ9pNisBAM!HLJ#KkcC&)o?9%HmBunrAzCf6W_Eb}@VxRb-tRGf>plmv zpZ>IH1=-ZH@r^}mQJcOjrECpANnx{WKq0ZZgjtZ4(Ds<|<;%<#uw=$)tVQ-4SWvEJ zduIT7v61c9`Pee+%vjK>H>*=qNM_%BQx}_0hXKW&SqUxBW-E_(%_=cAwbkV5VisU8 zEMw)y!j;94agP~$1ga8akQK21&aW82S=)@Pi3Qr4TC9x$q{fA8piMC=T&tD^STvTU z2cNFP?T6>~)^Yi$KZvZDQnoW=0SmHpd7K-^e*#-A#wlR7mx&dyJ^%EjHjF*9xZJ1I zvo?(7KYfj~xKx(p7-sDl8(B_e+y>M}*0^T=$r@WPrj?1s$gFEw3@999sjP`@k@c8y zowcoqby}>v*z-^28K;^>WOWgmS5^@@W?HYVIQT zP5V#Ii~9^v--Fp7TqMTs^dPb+WxF@4O16oW(7MF~u;Nv3-pS+5JpcCJrY|dI-I(op z3ePJdvZTeapOhV^lI7GCZp~)*o&BfU8Q`(Sntw?p8EkHD?t^D%$WGN8~_ z+s4)839=|{X>3H+%p$Q^##cS#J^_@WwP%dVb}g%{m{l?xkzFzyovqzG>df9lX9ev& z&W!3Yn6+)}Vit`}YAlhJ%1UNyH_snRX`L9WF{{+riLu%|Z*`Lg1L}uA476n# zXUh0T5n0bHs*Xiw4XtIiMi!G9qp^C;fKV%3lNZ-KMMImkSjQeSmfEt*-JRJ|S!Ksw zAO4X#|Dq%Cn1}dn`>^!&^o&KV6XW8Ra#qZ`mUUvB%ot$52(w9xOJ+Sz!3m(^Kv`)0 zV(vZxRFY%NCuhdw#T2uz=dJeNNo8diXKMi1W@~__jFH*z=pt5epkf(YWkakxvkO`( zSpiE{+|-I$FJ_E0W?b9c#jMX}eD3Bkvo2<! zRtA)y6|ORk?HMm*?HR{>a+{~RJmS@>Cl-s>%8ZLxr^Ot`GCm>vkoRvJppX4~+I?Ec zqOvPDHnCK)G-Ds# zWpSYD{7;N5iq|5x1{T{fPFuEzj9tyzfl^|;ce$sS&B$U*WA}Niv+N6~-8>k^@Vb~? zeIARf9pf}+bEZX$Y^TM^i+f{0CKfw0-oq3cSuFc0mF$n{%chu}CeMe2^vah%_A!2I zJ|US+ax7|<8K*Bxi|0s;h3s~WFB0QGyO8B<#<<2tHm-5WY)%06+G~P#4O5UBGyn9u zGvhR751H-EI6|A5MFA^p8KvlA76WR@EN4d5lKn(imPKmpV%8f2T*?|**RnauA{G<_ zi*}4XOaZd)^0dlYXEC7uc&RMrQ>{jJ)v}bafi_!a{|siG8ZVj6m@)c#Fk3q7dUk5r zD=2>8cW(`vkN-nFWSrKlnAMvNC{?kBRyNLzaS6^i_ z4+Bszi^OJmlmh#STDEl-Xx*62t7+Z4_bm*l_{DDYq?{G9P;2|CMV8dq)hxtn6|?rA zgscoG<;DNv7+LiElaTe$;x1=3|J3JyvdCWgPub0rwAj_GnY97+C*n28>b0Tc7ccwC z#8zf(W_if~iER8Qr^W3*IW4Z-_ya#i=L;_*@c4)Nt@%u4#ku)xDedx~f~>)IKB(4w)nj5A8{mdn{aWxR=%V=R?*X56pluDrNj|IWQx94Lk<@Qf*C-Q&5~ z8sOIKB9>a#eOc)%!xUPz%biIIE?Z%f6nAPo$X+7&q2IeTWIpB(Ime%%b|uGE%A&Dc zsbyWy`t{t)esVGEyf`PPFtOCLp|Nu;#XMWA$aX%xc6qeV*7)tC)3*hs4;aF_|&f-I(2m@##R>FaB2Tawj#m zXWTMNXBK2L|HPEBi`k4BM`fKDyPj2QT*zV>YeB8{2Iy5j>=&z$RaaIQE$pWvmI){_ z?687Kd}Ra|1|sWJ}RyGcee)4NB<$! zm<8L;is82WC-k*fZJOO@My+}lne8r*XBMTgm(+6p@kMRRti+a5R+o}lk5f?1X8)ZM zW0s3qVJpXYc~C+&mNDCTlol@oDpSVRSu|E_TgKh!QIAK|YW0hJKx}WBHMX2*5n>~< zGN16DGP8L4PrL#iD;gV>ZJ~8uES1g7V%(?X#-6BHj&a#fqE)MOwpz0v?@{}5|LaFT z)Njv6(b(P`u!Ppg;y`^qd2uDi@JczWu55s9_o+@MQ&};@?!7#%vYi@RW;JHaZk{S; zwE-`?d7K%evr3GypDHsJttw^zX@)66Y@ijeLiSDECwjB0W;07sLyLbXXmOvoL}Zy* zG_mgUSZ2*EiLqoBr8Tk;Tg;}I&B&tC;*}Y1opoNkQe&SPwUgtjWxJX+v8A(>6o*y; z8)nhiNsK?{Q_k(7^HF~Za}<>tOI?#1uO5$PpP)7iRv&K7W@ypo)9TGyWqWTx5sPiC zw3xM0XnzF=IDoePYyqzeSdMRwA3-0U4&iG)7~qGwWUM z$2^7fwJzwaM=2&X9%df}YkiE=crB@5Cx(nOTg=ir36i z9DUi6S+`|9Q1LMzv@iKTf1d~Z&Ht3M;;plSHp|GK*m}?G9$D;CcIC#|f+u7RteK4g z#VbR5`(4RwGGn&6^OjOp)Y>stX6(jn@?wzHW*#=p0_}I$7_emapZ_yevYw@IYD`{S zn`UJh*Gu!rfD*5#&N?gB!T!^+&mCgxY{pl0)*_o~Hq3_DPK;g6YB!Gx+4hX7Wp`?f z$Z}ohrATMijt=qdj3hdaV`9(kNbd~fB)0RKH_iuC(3$4W>v|qHcwA1p1HA#S?s44+H#ddYIsP>=O&~qWORI`{*nWn&liqKYH);cSZO`8Y*NyOGHMO60mpGsw|vo@e& zLAB1JvgmA(U2Ptu*8Y==*@bMHJgYI=4piDaf|g>|sc{;!t2G?V)&S#O!eFXm-t^Kx2PP;!e|>GOnFSF^5VtIJcf6i$qv~k&BlK^94M#8);JXU5i9YiyUYZ9r8e+d8XCmbH-G zxp6klN@uyS^mxNJ9%?^6wKts0$Nl4G>+N@9H6klivpt{;<1d12h_(MD zWD8pFnT-RLS~l}fu4R=IvvO*RHD>&J>e=0zJ{myr?F@UW+B*k3a z=#j=Qnf-xE*(Anh_Fw#qxBjKx@{$3jwhUvdY)w-jw91T0j;orr%90ze&a6G-KmOyq zE|06(gWA$r6|+A7Q%xFUKQUy?(M#frX?#vBE|vZGkJb4OiNNC?^f&tB&WYhQGOM&$ zHSL;y!Z2=)t$9T^c{nt(aJvVf%7Chq$%t8*Pn+4-+B5F7*!y{w0hJ+RirI@=_S`co zWHF$e8hevF%otdF?*s1aA^TaW(Sqh14WcFfcF)DlXdBiIsTdA>$bs>AXGa#vP zfVKV9Co^vQ>6lm~Gd@pEL0;^=0qAUH#nM(SPg4waPa^_1n$0Kt@^WoEPqK}rvN|)xntv+$ zDf3Th^1!QWS*ONFH7jQIiUF2c*-wb0 zypX+U%#s>=oWhlCZ|2!Do6{_k9Isglm9x@Vidi9B4pcm2_jppv`o*+p^fa@pHA}0< z>)eEg#Iiy%5^;WtAA? zKefyXTk)#I*dn`##ezz1oC&Byi`$Y}8Bm6`w+7JQ$<6>|*5{>Aa@@pbGmmK1>&)&^ ziaMLIvtmvrW06gh=j&g8UBrsn!!ll*dA=oD-RAN1lYup~8K>xC_Ra4)GfpkL2aPFa zofm_w5@SwGVU;zswF9r`jCCrRAFho7>GEJ1Ytt{-kjSq(txoNd|7 z>13K&&M4zSiqn1KY%<$`(kum*v5?h@#O9Tub2ei%mX*xdec76&_y#T6dilH3SdTv? zGj^ND+wZWlsJC zwVbnUpJy?|9>~_-0PH7a#%{~{;LI3msbyWyN@PimofunYQCY?*(&VworZp>K<3EX2 zFBh}_QoM4}#sKUms#&*r{wSqvZ4LliSx^5a$a@1lHy3%MIF7{OfNfmhbypTV$oP)!|7&n?hDH3$Hp* z#>O))$Cv~eSYsKJ7~??YB#V~WYRlR*UWTz{wuoh80Mis-S7(;QSj_gz-%-of$z+OI zQe&?av(2nuvywu#RrY(r6=L`NlZK3KKdH_0R;9+?&I7WB7L`S0$&9NpYiKR9PK#05 zlh~f6=&b?!Wj;Vwm+JCZV~=`PrL3SuVS8+m@h4O^%qAlqjeUx|@~z(Y(SPGUDO}x@ z-LA0}cAZKl=qn9cl?XDI~i*TZWv<1~35_Ma}= zJZj8pm|~x1@w*aQpv^9KnAIE1)+`04aqYiTF`HqEnlUCXw#p*1oU+X#Yn^pg%;{uu zVv3%nDEEmzkBQye@5+6u2`IZyt+5odT0zzh6sRgE=CBvY^2W;1cjxdiBXHxeeBv)` z^QjCd5i7Z6iB^kjyT+Xun^%ZUYRpa^uX3Pd7YkVFY^$sdV;p0yNsJrXGyqQPt(|`C6wUg&hS8LXeF~}mbW>&l!TG85$@d@t7 ze5}qtPXwNbz|H%Ds%J%O>?ga$rZ$E##q6$T3tA_}SWo1}Rm-NygUp&)6|=Q3z^3tQ z_)qmBA8E|iG=+us@Soy9sglj9DFST~Th*+avug3|E>Bmp%8kF5)c6Mg%UZ?kTli0% z7pIz)%35dD6Xhy4`)31orxU{=J+Fy1nY(3)2pP{nMCY%`lu_Rs#z15it4t+U3p zGviD@NnzWAdWsL&`Bp!D5&<{t^K1U;vl3ZcDCfn=jX%f8BDJhm`A?LyEG5PVvo2?8 z@*uQUS(q)$*uxaTmJSc)Z0(mdvhklzX0tWGD%<{3oqpURn+d2`#^2Sd#CS8CV%Eqa zvRS=h_TmIk5W7^iW!C!wAhtx7PEQP|A~wSmof)6wPcn{6WOd_#MUOuP*$gbg>lKmx zF$&vl9_rXBWuNl%%nf7mNxv?;18Oy|7*MtUF1&7J&8rW{Y(#b?#>tGKmgUxLJmaWr zidnFgzB)HvXGV3Gr>ohhY$wKU@^Jnq<;Hb3ne=7dn?0Cq2TG2yR>^E5D`M&Lyrre& z_-dOcTLa=i#V~ebw$DKB4$sbuJ!QOyDKr67QeVgDV!LuY8J;hr=@Tyt202!GmZh( zXQqHxRt+tx%Ts-x%8RR?LP%rknLg?nH9A?OhI4P0~IB* zn8vG^1z!={5KSt+bVHf>qgvR2uy zWh1ig@lemUVVndx6Ho|kOYPHt+`i1;@#YZu#NU)Ub|uH17IT}dSoV`HSiV0S<5VWF`4m2VtnMr8GsVA|FgS1 z_KdqRYni3RgV0)L5m_`g<*WxP{-~IZ$}&vR#F84fVaxzjoteUxaizwLQ?N1MfA@@W zpmk=PCQnZO`Afw9uNo*uaj+K9i@GCz5b8zQ8@%J}(9E7PGCehhf|@ z+sLMtt<*TBY}-##S^H1ImNt)>Ez1}SYE2n)GGlKINMc+wi*+(%m9nfb>j5Y-$ z{~4XxfBvRb)@gB18AoKl|A#JRof_AfQNyf={Z~HCqGXmuT5Os1$YPza&Bbh}T_=EY zYOKUq3!TjbR5xZ#td*8dPa9Bnpv0@&JfgOQ_Oy+mwfy2PXKh10lgI7L{7r8Lk{{%^ zBr}c)RV|*l#mh9d$o7zNPb>;t4?wNPY>iX6ko_{m=FF(;Tr$<16|oXouAY7}u*!>d zIfoYUj6-a;26+Eny0b{EYuVVwv7f%%FEiV{S=^_ntlB&-XHS`Jm1Q>%#Im-`+A{u8 zkgd+Fck`r{{o{ScHbj=yX|Z^X#x}F7&9i`QWUJ59Cs_p5l390n_UPivjM!8E+dl{m zH{feB`*gLI-F`8x*+3irDQA)a*c7wb8qlv$t|eM=WDJI2|}bB-A!wB*G} zjl*oc&c`yKtguLI^=22c>~W9IQqD5}gwTpvE~Ldew8k#SIEJyiJwjI4)|$rbQ>QiG z@CNYsL4UT^BAF^?H?TOywf)YvF_o;$C$5ZAxI2r;3Ro_bvMOdfE5uT*{!nQwf{t0c1=K$8kYf8FQ(O}Ah%;oYP`10 zT4%d6n`&0|Y$03oPoAYvK`V`|VpeG}moO`#{gWm(rg0o7j$&1P9;nq?^(=$N_Mdip z_5gMg<6-uF*`R_ z^F(NoS?g?V=P|I9vn&I1Z#cU_XXj`d=gq~paPNoO!a1$%C=+Nf{X56cG2Dr(iYF5++S)I%HUn8<03$!{jg@|p(*gA{(q-Bk*iA97~i#k>-TLY99 z+ka}2t!WDHb1%>Mn5F>N7*M?l@7X?lU&`-(W3YVRUv%|&tgtYC%vA>@6rI`b^H^oiodKt3jL5o}{rAd>11+Vj z#*Dp`8Mn+nGysLl?iG#IIK}jNp4s!s`=W3&K8gR-Q;RLJD>o)9&Md{1=_mZ+Q)Y8s zith4!x&5cJGm8aPnX#Fz*O|2iRkIXpngU*t+3i2k<{>k-$|AEm%c8nGe$4PK+(IyU#;n?9(iEVjPXF0VqWF-!ZX>&`M!_$T-E) z*)(UJ7?T#enq`=R5vb0J-RAjmZwwH(db|#`&kEX`f#v)DmJkcGxW=2=epwz9i_rE~ z9yC@eE5Epqtv-*T?emWtSr@aZXV>e@!mN5cFuS@uys+%_7i>*?7i3`Zr39BeLYh zYo9yCERHcUo5a}j6iuvB`RVwEK+F4I_s@u=*x0#@Ib zhx<)mhs++uEYN-yiS5id#q8b~;6@LQv7t?4Hq5G+4YUBeTw__rUNvQ`YPMYCcArws zb}@VEY#~c#jLudqYX{26qOxzoY=#y|j9tx2WmU}T{NuVBSThT-2C#+0!e+DPp)eB8dw-}>hB zYGT*SVh=!F*iWHWjhH;8n32oU=I+cv4j5D=JCCj2SThwwg8EtYmw*6vSm&l&A zY`Mlb#@J6=Wxu0A<1~4^W401w*-yo68a>}nYRt|6TgEzCW4oF)vdN5*S%}q&$o?-i zvWUncvwZ?`UK*Ne7G_IjyUpWbw(O^Jp!%e3w0Th3=xh>Wh$Su7y0DDnJt-%a$d1lF z!zYy+&TQw!WkSV*QZbtu<1S@kwpEtAnAF(SEN7-bX6-&1+T%ovjcj##x|l^~C9!9p zC&{s0W2>wVtDZHpWX08)EuGaV+p5vixp6gSzwgW#o%IT{_KeGZie+5()4%$mQ)72| zv}1M`vnIAPFDWT|D3 zVwO#_2rLpCYBNmHiE&r6DP`Hr!_vv0Qp+~7C9^P_CXf3(VHT0~qA}ZPF(TVKn`%~x zan-UG*|DFVDYZA7Sy@jjD?X6LEADesBrj(CiE9&ks4Uei(AN2%$ca}mOJ=N9zzSNJ zwamtV`nvY=lmmszHnIS#ixXqaVphGJ)~aTuvhj@H{O-z(g{<7CEakv=r{m`dl)#nWy|GPK*k%y69B{>=`4oYnH-Y9yYln zvu^YJsb?u%$|A8Lwp3QhFbins?BmZA*81i*m|4asL~KK=gt%pv`KQW`BeS^1g{-N4 z#g?&Z+0?W0pAgzEW{qrbz=PUm7GzlkY;EPK%veerW=V@JvtgEcwviRFT4h1SGZwPi z8Nf-~zNgM?C&t~Fg;@JfZz(hWm;EvyKAQ|lF|{lwAkXUUJRmD(Jxme*sb3h{GHac! zfyL|$*r~Bz<%1dH3(Gj}6Qp`cV$JNaXZAqW;69HB?o0eLZZNY3mV+0GahOG8f!4T| zZCuqXqsD$Ut)#~3@KioiUG=uUpKPd zBa7eiz#_Qns+9Pj);9O;8Pn#G`-IBoG>h37pf*oVOtBJUHsCoePHwFIclb|npLUaH z<;6~oky$#kwHjIpt*4)`pD>MgYu0CfQhMxH(~`iZ$Ae+~r!k;9HNF&CCB3MHo3vff@&wnh^&@wv|v`y%6QTYl&+N(J25U|qqC;fiScIkRcFTRz?1jnO|$jV z(9&0Lm|dq?G_$XZSpnN^9u)QsCB}lb=byCAJ@!*+tj8(3nia2F*iYFr>&)1G@eht- z)~2y4S*qFf%FsChlrv)~ZA}@+G?oFClNs}3TH@B3v06Owj16rMQ&7xil)})SrEC&o zS;l((J5uA;+0?QvvrdYC9A?Rlp?20Yjld0NmR}uEdj;4|it&t1Y>*{0UIx?`4`eGV zE}gagREDvE^{csm)#ek>dc}X*+uTEJPE4VV?r&t2q70~&7SoyKWj;K!xaOZWvt-7# z#OpzrI20BBD2kGQ(MTomgO}o%YVW)c4}O&89-j_TGrTh zT1**RrL4EP3s{gfwZ$y%6N#}-je2oriug~b0j1RVJ0L4w3t69MG2W9@)+c~s3!a$m z8Dm4cEaSIGj_p5XoWixN#-FN~jr~*;i$5~79$7rkOd*}sN@g5p(b$7nxyCCoPG(F} zT%Fk#*>az1_z7qc+S1u)|8Sk3pa|StX8FxaVku<9EOqSa&5{>mJ;i-$WK+tnSqc-Y zuB=bA=)BlFX6+c;G_I4$WMUC!tIhL`M)qj)sGQXW&v?&LI5Q5iQQ0;Aw3sC^W_^D{ z3$i9QuQMxRn^~8#6th}x^I$+7#cYt>)oe;x1KR^oC9`H$)M`m#yU`PwjmU~wuj=wJ zu~_A7L#tG{@?x9D!_;$#z)fcM(AkGXR^HQ9WYd_H#_HlpiZY*;$R;svrFE0XuMBOC zEurm~`RKlEUS<|-^IBRW)=OtuyqEE(^(wQK7$-N*m@(Amq;2t^dXmBwEsn8Y%w6TI znXN5&*iT~ihp232#vse~I}&4E+~&!i+4N>9WLe3JD>bf-0fH86ofj{it-Sb8QCYWn z$}{f9EVC4hQy5r7tHT2G8xNkC&m)lA4p|s@_5cT6HqQ@_e(=dW3}AnsU}a56|rVkec9~gsbPvR>zAx- zXq_7)vtG`O-Q}S*Yg}~@wQFS2vlPybm&_h{G1cr~`y8;nxyOEn60p zWfq0iwausWWv`xrJo^GXuGpTjn60VBAFtFnzz(&~oZ6eqY+Fz&W((PrvMOhVtTW?7 zWo0~l0b;*c{?o!1VpGhnwycOXv}*ErVi9Jev94uxX4I7#SC7Y@aeA|z8CPQLfki}i zz5LxOXP3&loE5GHR$FGXsAf~lk{bIZ1J3PtRm}d7mxj)maS~%?#%FKVma#nJsH~{f zFa^+7Eeo@aEWmR0tYQu>W}O|A6z44?#vzt!HkmPf*|CkEIj;TzH(b*{Ey{RJhHf z)+~E@=<@V1g`l;>qOwgb_S1oE5leTrZR2!%mc*u>WsG8geLlSMQ*I`+Of9k+THInS zscTH*Mi$Fh7dG-_S!dJa>8$}+P#%Jk`D9*Y8QV3kkwu7IGK*&%ofWcFvoM>?n8bJ) zP_>(94;fo&of_wrp({1+Q3_ec%s+WaW<@N;tY!8s`aEg#lx2(oWoXwfclNnkW?jnG zn6d09ZNM|LmRUPcP)j*$osIbfw49ZKH=j(#*lPaCjUH$`nn``O&Ob6cy(sXK6ixHunJd*6|h=jRaeflXqoMa#TtO3 z&*Py*QezS8YW4!zgV{2lBD45UUjO``8QC|5tn1mTWg(W-c#j!JXTes?9;IwgQ~Xh& zRa(3f<9%jSHG1L~$9}TH#xySX$;j6D6C&HL@fKPk3$!+kEwT(#7~56I0<71F?DL{E zKj$XW>hnLf$aY%nv$nO$c2=x83a=8`RI}w77qphxf);&!m01e71=#pcOJ+f~RMyC% zvYZ!{)HqAk?3y!f$5_d++B`yb@0c~P@LCg#Ms}&JgqF-0U|E$KA7`eJ$_m-F|L$^@ z!kKYd#u3@5Y-h&e^-49{sj-ULKFgx)Vu;mBRvZJWm_1VCf>w{E@t&R=xHphl(^{MG zsAZd4i!3r*Ld#YjC&oK7mcSmp+16PRTS9Al|z zKKDPsGS;;;7H+GUMP-ew&nENz$Sl+XEXZ#AY0W>88fTb7$a>4{zfPAY&?+xxF|wE@ zkNqdvPhz$$W2Py3UjPN|GN4Lfsb@)z1#6h?K2M2k?{RmF$60YH?J)b?kp1aRWH$Cw zgjPv0nKAQEtCqFO0&Ef6!xZt1V?WizVs&}g&eO>;u5somth9K>l(VX2of&&CkBiw| z&BlMK#5faBcu(ZT-_fN#V-xGmJl}O}d)ydz=~7Dr=dgmSx3(DrP-Q;nu8> zRcb6`Z9pwz3)$4NWX4I1RmnOxPA#iq7HIeUQ#|8%P-0dq8q2YZS>4u)%ysy!$*U zWzRlO>RIdT%8a+jdX&P=*=9D=6scqxr6{45|8!Z*dYYnA;{w*3@0QGVEt@V+zci0k z7MazQv8c7m>JVm8+TJq@vnghmV{H4$sj);>GHa1tn|a*jX>4~ro5a|z@u2h^BXHAn zR+AL^l|fYtjm=F8*~5WyYc`#pI{(uvqSlUa{3p*pHMOrcv}kOpYz+3XDH7y9@Ei!3^uf>nb zbEEcCH*9_SQ=j_ur#{U^P0?`Roppo>+1$p zt(_FG`vLm1pZ@G;Kh<^C^NN54%8zw=n_I5R(7Dz(tEpo>Ei}{CArrRYREKWwxW9u{ zY`fU%s;tH%RWA|v&rge%e@3_}SShcO!330|4%`wfN zT9RMEtSa~W-*?hnb*^3IAdROi5q?V-Ee_QT%K0lwip4jtlZPY+^t1G>zEE&4-%7y4 zJUMkI)|FTLriL=?W}(;a@^U^YQtesKF#_tu*V2h`?Pc`z>eIjNpR{hD{*U-D-_hA+ z3)aOyr0e-17OCn-T5PBL(2J_}TpEme+zP`*^5eb;5ns&rzc@Z{^)Am7EKgJNNk3)e zWAAt5EE`%^za9Vn5g+A)@3ZQsp5KhG?)UvK{j2pozmuP}zpTG}-SR2_3l8g}&*Dz~ zl*fGQt3UkmcX@BE2kYM4nFve-u5kn&+`oUHU+=+#ckjRFd(YsPeNgLu-{JQDdhZ$h z#Pu!tN&OD@z4SNNz53Mg%jgq)_P&q(FR$~h{=WKSuHNX+)St2c{*U@sT>ZY6@2^;1 ze)w_k`Iq98_usvL|J`@)yLs>v55|Jm6zm{umH85dj{o$lU(i4AdE8$8*-!KR z{{8u$egC-aA5{T3KI6On>Far`U$K9&tG{@?&*N`@QGfB1Z~5o@s~+>sKiY>EHo5cB zzR3stS66=4m3NQ7kiYdH-e0)?{%d#7PsHc{P=3P?7?r2;WzRAu{R{Fl`u=g-Ke_^N ze8zYC)7SG>zheJlSAX$~_xp?Lme2KvF23&SJN-o;@%HMsyLRti{#{ppz01#DulM}_ zxBe5a*Y7sJ%|u`#@Nfi%*@t5?ADIYT+Xy5}y2zET?e99j*yAJc!4f-t)#Ja#&l03;Jc6gD<1oaC;X8g_1`_=+s>yy za0K$OyakWV5BwL;7fu8w0uzCWz(imoFcFvtOavwZ6M>1qL|`H?5ts-}1SSF#fr-FG zU?MOPm1qL|`H?5ts-} z1SSF#fr-FGU?MOPm1q zL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOP zm1qL|`H?5ts-}1SSF# zfr-FGU?MOPm1qL|`H? z5ts-}1SSF#fr-FGU?MOPm1qL|`H?5!ey1qL|`H?5ts-}1SSF#fr-FGU?MOP zm1qL|`H?5ts-}1SSF# zfr-FGU?MOPm1qL|`H? z5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FG zU?MOPm1qL|`H?5ts-} z1SSF#fr-FGU?MOPm1q zL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOP zmaMBy^758jC)zx~cb!4UcHh*uHz&fD+Z|J(ci zdI|G~TEBeoOFftL^T*pr?%RLY@4olmI}iSr=K;y_FW-A-%kPK!5%a|pfr-FGU?MOP zm1qL|`H?5ts-}1SSF# zfr-FGU?MOPm1qL|`H? z5ts-}1SSF#fr-FGU?MOPxGoX+>sb?liNKADz`L_10uzB76@lxb_w##91SSF>ECO%e z(DlwcAMF?asQ>N>KkeNoeEtP|`WgMDFW@h!ANhA$kNN5E^C|vC|GWEdzkA>B_6HvG z?e>SC@Qw9_@4WkWZ@=^QFCX`t?Qiw;?)|qLy8ikXdiw~!`$zqEPxw*4`0Ky^#S^~d zd4KvD{iV8kP*Q}fI%g%2(5ts=4{1;ghiHX2OU?MOPm1q zL|`H?5ts-}1SSF#fr-FGU?MOPm1qL|`H?5ts-}1SSF#fr-FGU?MOPmg=g=GZA=(5%@3v?+slqkdL4L&3`|a zUOn%@{RckYpEVJ9<`H=Jog2E|edeKg{15nGuW>nFH4&HyT$2bqxO^SV2lwwk(9-dm z{C@LmOavwZml1ey`Rca+{cnEx?t^#!$KU?V--+8<6M>1q(}}>PG=9AI-n~TlZ~pf0 z-o5|MFMs*BZ{L4+)Q_M&**>E-{s@q>N{S3lfP+Z-|HVXw>KgJ{{MRKf!=1lWD)q; zU;O9){Gb1y|6A)n&APr3_^ZFt@Ag;!>90QU`pduktH1h7tsJ>`W&Oop{^ei%^e0^ZuaC9*XMUtx ze*RkewJyKO*T4Szv#w7B`0wC<5E>kyz<2_e!=VWU--i3S)bF} z=RfE5+0T9Uv!DGOhtKfl_36)i=F?iA`t+x`u>MJ}PpE?o`mg)&>$U#jumAc# z%=)!o`-i{g>#zQr-+uL1fA!b6_YeNTuj-mrcYfs`{K~KVeP4b>i+g`Rm%slJ*Z=hF z*0*O}#|YfJ_w9S<`qsT~v2yg?Z@uxYH{SS`u5Y~Y%{RXJ%^bh!_4+qo|K>Me&-%tU zUVmLzy?x{BU;l;1M0b;o(KFy@T!)c3i^qF zr-6DVctuN31JK{=3*5<5fu0F?>t};J8GJ6!1Uwn^6M>%zcsB6UK|dSp=Y&sw@}Kyk z=Y>yvf~N$Y6@2~Oes=hs-_={59ezho54rj|;;x=2c%tBDuRC||-nqM8Fqve|sGturKNF6i&bWZE$U3b^9%|8%_f)oElgTqLpP>eH5{;`>GoO>w{nwtzcU# zYHnDUmRrijtyujUzlc)V70!OH_-~OKDKyAz9r!$ z;Gz{?7qo~s^ft62SL$sEe}s;I0&WXgxDB+w^EnER$%hM z7tiOTeuI~kkL&nnL#l{1vdye`g;^vvH)dC;`i+B`MQOQ8Uk_qiVWqKFS!ryr{ryhC zEBQ_aE?P;zwZbdNiq(LtT-z!uXur&xa&8jtB->%O@@M`jGLFl$`9MqbFG zw213cSOZI?+@-QbR=|o}u&u;6sj(DxSIh`3tAXv(8DLFq$Q849B(PfH)<+*Kw9q=t z`cXNDM@6786|@)33fr@cEul58QCAXU?y(A5Qeu`1S<5W6CN&0Gy%o1Iy2|H5X5(-r zH3nBLQerVHR>_T{vfLoD9ANg$iyPUaoE5WG%L-RBi>akcugI*EW8-RSofd~zkd@5p zP|PB>#Vk5YYHVT=*(@`g#Q3){yL?!Vm#Gz~zYVb@#ay_X<-8cB&E-+~>5Kf>qaHl- zu@@8pVAVx(t3xlGCzRF>6s}VgwscmUHm@KXh4t;hE2*(BrL%~u*ICO_$$EioYT2q{ z!4_VP?DC-@wu@P?WyO0+G5bZAvt-8Gh>rIZGrEAa`}8^4&?T~#S!KptkXc+O4nj6* zF(PZF1yy*B%63vLXvM46l31xN(B>MA#e7<6vDk&#izOs0A+DPj$>`Oe{em8y^J5>5 zfKWB8IU3&L_ArbqF^*wexiJD;>oAN-jL+5fQ)CuoX(k=ihFDj#F^%0rDqcfu$!z4+ z#HOMRu_VUgHMy~nO(_eqrdIw_+DDZcf8MpMHCDi$_EXF!SUpT*L^jw8Q(>D0s=Xk! zbk@L{T!`fYtUiz$TVi!!-I2UPEw|QI)vv6Oat)FFEj>VHU5f|+DZhvgMm5LSPfm!d zjJ41vH9oUqpoQ43Wohy(YO9o`n%!wJ)K*@++@}a_vSJcrE=yy{h?5l?S`P7`glq}z zR$2?JEn^?T>t;3uEh5|1ETt@BYwxKHV->O%TEuqAY|+Y%x;#FT60ZxWZtuycme9g& zNvydwvrsEsIcm9#^|6%p8bW(59o+s))P5#4Q#Bu*rhD?lXdp!^1!WWEn;mwHL*KA-ia|9TRQ8mtTW>- zW>MK1(ty{lW~-Ks%6^(kb|Fh*9FjJy`B zVB5^bc>>(4S?Tq7e{i`T5kO;gfmoyK3YIDs1FE?dq7mDLY-TCCqD5p2+o)_48(2$b zH?mu3O>L6n5bY)9sn^-*KCA6IyBd}U>izBk;c9~BR*~*K7Hg#-d zR>)qJS$N%fv7lXfaV5ppSY+0QvCStjtE{;4Vlv}YvUh66AnW-jL(BYQm9w#*T+bTV;93Th8?!4Xw#cTOwa(I)jmr8M z4~jarN?AKlB*ZPUv7nq82irJMO|512X!KkGHoOYiTG3W2+Q;y69X>p?T$cz)VWYG~ zYDAXfS|T>o>gaWt#&l-4$Vz65S{ueSOMwlwRMy2T^(-Tc3)@9(C&g9GidlH|u~Xy7 zZ2GcD?9y0fpctm8q}bfj}MH$5~+(>GUBp~kJPwj)*5S_O)a~H z)~PYDYT1A4a@O{fRTgwDu!i=~*tB=9GqJxl9@KR}wvu9yMQagTuZ!WQR@vtDiUDPP zB{z=HmIH;zc4HO;YKg4SO)cx=YV$~5BeJ%is+Qe}ab&jAV#_Q#YhD3%L)#9NaQ&is zvdN7NE!_6n6HqdsQqQ8U4XufFUv`zVJz^Z8Jrm_&W@85uUZzB zZJ8x2UesD>Q_PYcJ27sVtx1Z?j8RxE++rU!`Xpk3)-9fNd91P}vNoT9wq_|1*o-Vn zW)F?E`(y{Ih&@YLnmnBvpBn?nkA-bDcrc<&Y-(B8v(Z|YvW=~T)h-mbKntUH3R{3R z9HXmiE#t# z4v(<~)>N~=x~f@vv+$a?PK#ldzHAz^$&5p-h1Szg&W{n=YVwr8+J8DTQm67*N}S5~v~;a(gK;7OmHw+b2Gn<_n&61k7ug1z1Dd z_S1s)jRow&mfZnD)ee+kHMT%oX>m-bl(S9kDrOI2J!b3?s7j18WSquqs#%ib%g*fb zj6Gz0RI=U^0J97%BDO3~8Iu{~KRGw1$J5XXSKCi5i zEXdk_ipKg#bJmG*{Nm*rZ)96%x5Q#T;Tf}5Th?R7l^5HCLTo*4ES0s! zT4gC{!|Ns%TvN;%*@AWxi)FlP*;vL-iWjpz{e*4Yt5W0WtXW-YF)f}8rCk~;Vi&Uw zYt^#wiqu|TZg2ME{Z{<;Let1{Tf}k$vyiJrTHLrsX6ef!u_1Oz?B=#r)(YDtZ8K}% zsdM8hWT|A`<+03mIZKNtZ64*tT)eKT?5<;-8k<)W%i(OymiZK9A1-HMR?tFgk3U_g z>=s$KdP42VZ00Dsip^~mvUZ<#S`4xeOYF5zi}idmzVUUSUZ@IIUk+%2wW$@b;r1wJ z;T2@7ob5gj$uVsnZUk)Oiutr0sKzy(v7jY2c4n;O!K`c9$n36U)8R=m8^^fI*(zkC zvkxW4PK!}mCZAX#mqFuBjm4{(6|p-t?!1_d0c$a|*vML8>rl8Bt%&TQvrVj0VgY+c z7hO3D)q0D-UTc*dW?w9`2&`c}SVd#C7=Z%YUdf8Pm^G_nwsdyuYa>f)d{N65v0~ST zabvqwb~CHtr;=H_PnOvd*|^2diw$jO#s<5~w-v-Eg+{%KXSofKPOx5#>+!rs%$jko*c9#0c1h28b+_MhSzZ>g=U z*f+6@Po^;@NqZANZ)hw2AsP#0(MmDA~W5!C0y=)sBTIa^@%HkWF z*9C3dCvW80mT{0>g=|_pB*xX6WzVd0<3nUEux%Mr%*HS7LiRAAqOR+RzM5Ix*1~k+ za(%t0ekOS_kJS08D*^?rB-Z2_)rT@;L%VBP%WN|n)3}j^*C1PNakqKs%tGv502^j; zpKL)Pvdc6+WOl3U-WQ;}nAEr}Vs{5Iu@zN zO^Vsdk1H{zm=&&GDP?Uy;XXC8@Y=}c^iP>s%#l)7)Ee9xGDc(L7h7uCFng{f#;eU^ zV$oT_TFmZw#Rk^ZY?!rcjLJ%4Q_a?xv1?gq)kp<>+4N?QuB@;<<|xRFw`q*|L~g8H z7@_qY1KTBK-ikcy)Rn0;y0(*h1d2N}kHqXwD88mLs*vM8jd(oTq{8J^z=Ly@avqfzWKzY>I z+3_9a#kwqwrJ4m&4%a_5o;?CD@F_(4s-KS5qO{z2sfOitaT{18uXQxANsjkC1@2QW z0L!T<8rpSoiZy2Jv!b^9w5wU?#mh8~$flHy3Du5qkVS2GDZ54~(Anzr*njGBwuxO` z9%RJE9at*71Qp^z`VTmLu`}Uz}hY@m33yUyjUvRJsy}XtGI8_)>>kf#F(pqUBtSQt!nnDX~~XB zi#M`6G4_bDH5PDJIeXYo=JjA!GTR1}^5Pn%P)^*gv6R+AyG&z1?UhYQk6FdFc7x~12n!>DW zTHcAmW|AV@E@Dq;)8`SpnWN~$xEy0itra%NDk-Lx1=VzBQ`4feOJ>>2que;PY;l`? z0c9AM$gbpAs77O3WhJc#v6)(=h>c~eISMTkyJw7D%(|8huohZdP&iOFiwjxIVhUPj zpYV(inRRMx^J(jBc~2Cy)>n#H&l?+A9mFdKZfm)c_5Ev3Yks{K`Ivoqf42C%$6qg8 zxtAN=tE`xcHj7I35Lr7<-J8v>fC5)(u>@8NiG^9-dR-E`oa1tzVnM;|VHxMds1{j| zK&h0?8{6+v%0_ANis5yo#<)+CSwi# ziN%D9#-7kFp>-Xr_u@eT?9rD!iH)>!bY2V*H=vxIdiI4pahTMGR_GdBj@H`Zb=|yC zxp9c?oY=7P&dQ7{FHRlXsj+!oI=hD{EVF6w7+QGk+}P6;%8XsgR&TaWXB^MCaBZ12 zw(iR2U}ifjhS+4qjck>%sb%-RfIY7$TCKDeTGC?MPaq4eUanw$u*_Ck>;_MbD@J9D z*Mb&x_44iAGaVMSRntbyuJQ4OJY?sGDgs+$!)!Fx)>Esjsl_gKW}Hz)x;=&M@{H5o zL1Qhm5?D)ZWyJ-olVhjEDP+~0&9(p%of`+`5#dsBK^)w055;W8*)O3-^lcL>cSr&WzF7T!L#uYmL1L zxcYZ|0gu@Ek%g;JHLZ(S$W2CkDy?f;H)Ok#ZJljqY4OA^b|Jef*~`5=H3dZ(s}*cS(wddd8d|kwUCO#KyL+?b#n#z+#Q@t+i`wu?C7WXQQH|N9u-PtK?o)ZjR@jr- zB*rH80@{cy>2b2-1~ye})v`535o(R=9W++lUaT8~>oEJGo;|{Bq*j;~vj8hr8(Ys& z_|UD{LbZuSXI;yJt34#r2 ztHz(gZRsq)BDC=8gLnnqZto(1gDmMva0|5UIVSy5V^ zu@$yvpy=&n30K-Y%7~X`jB||ChE}UAd)%ED8`(24c0p@o>G2%QBCuMFQG{*smB+TLY+Nv%H-r!xT!4YtDG7?3#XxXB?$1$C%AL2eL3rVq69kwJdLk zU2JxZE5tUlsbrlL3)d!gGb?Ii8he!DLS!4;Xlz64_nKP)o7}iPD1a5UYguWX7(Wt? zYx}{MoShf+=YuTFZe;b2v*Qb)O^?USUT$$e6c&+P%;FbAF6+=(Pe5g7K>Ni-ZM>(h zWyPzrW5ibS+SI0!jm%yst%X*s2G<;2%PtE_335qo0~=yL`&l$r`7tT7mup#4V@%^# z+T}kj&zL&4c|~aPpH_Csi#I0pEN7H#@QsbGac<$6zUMyy=jmEjxH7i5G}aaE7TKotWOgAdfkkCkTHMfTQ-Iy4aBBlyUt9}rL1_}_3Y}*0&O&QGrRh-t1*lF zR4Tg+s3z7b+X}1O%8aA1Qrk=zZ)h`VOhIeA7;P1*by!^<-Kk2}uCWDngL_Bh-VwWt zUPBvZ4=d{WlRF4K-v}fh7Oq;0*@tS`AbS9NwKLlTi(-~l29)h5Ba2mR4=SGVsj{^- zAjrb4H8$m}TFl`PS>25Th5b~}N@YuE%Q04J+^t!7EoQfI?2Nb$#cY60H5;96 z3o6AdTD!*c#EW}oBELv+d zw9bkjs%9Bmq>fz!P%AH%#71TP!aPcg_xuySF@gWnT+20<$`-W1+NN>YPdh6%vEAkIW*(E81JqV#%x0c5Gd8pFjf+;0ttF97 zHr&U>!%evJLLa&>HUen~U zXDm`tTTAU~^gyhZab2_?I$O{(Oc9m6yH*7D3bdrhH*vj`*u1bmT4k&+OJc*U8$8n6 zUT-`kv#qhtio2L?nGLlkvBni-QQ4AN6juw&c-OK{kJFj$UG9{#D=)qrGj7YcTC-Kk zdH^a1w`D`EJ>z8=cc;fHi@p}J64>&e5ZD@7Ja~0h+>Kc?yHwV>afxhY$4h6O99MVt zu1H;%=xo`=N|HnE4P*C(J$zrbAB?uLxD}~fvr1vlNn_{6ofjjr7o}|KS%IrMRw|3q zDlg`6p|M7mvr>fBqmnhVc8$F^z{uJ(PMhb^jaiw-?(-bO6x)63y*v;3Ps=c-hTTd_ zHOn%wtDH5qt2JwlJu>5*WO0csS+O)WsqrdkQCOU(eYmU4*l)sB_j^HWBYV@d){mn( zJg*1<>&Dg4N?a|mOJ?z&&IO~bv(@KeXc1W9btT6wwAp`GiLrn6zFztvs&>>&yOg1gmJ(;awmf55#&w3p?LURts%2}7do-4T#a+s-^FIk#pJj2^vu!_} zUD=fo1F6=X(%1u7NiED;Xe&9sK;qiJ^Rlz_T>t)N^`7ozg<99K(pg3+qOuFwBDQoE zOlz9LDjTJ>{j_pp`mzyO0o#eOMK;B(kdibsHo&H)Jq%+DZKEqv zcWYMp@ZwgmdL=1_*_)}h&-F3;68|tX7G&#K$m(Vd7&o&SeiFPa32bg)m&ABa8lRpq zGP|nTW0+zyi)-8^ZJWl&z@kt!u(WwHP_f!Pl^3g+Wm`aw(ODr&W~`k7*4c$@&nrr2 z)9C@+Zt(!E6XOeD5K!xTm~V~fQrr;=d;%FBJ(s%G1SvSGY+wyIhB zvfY`rZ%k5btqrscKvBawE8ZGg))Ul9VSTju)RPnqY;MEs=C(k%nHtM4|C}GVFZYiV ztBCEziUH;97-lW2#{p^0x~OAujLUoyuy&2Rr0u?}tJ(65$%OR*y$1 zRw-Nh3bg)pu1eO`?BN*q&HxJ8ENaZu16X65*{6&c zF9!;*n5AI~cY12XxUpTxF3)()QjixrG2Z(EDlpzw@WF^grqRJO(z!L_fa(iX7JjaAFqF*dYuj5{+1#5kVuQO2e>i_ofo%^_{s z%8r-$WLP^p?s1BRZEfXgW)194kKN>{idO1++2vu%c)L%!mr9njSX*Y%SeWJNeLPES z(b!6j%`7{5iq@sG@M?{PT)}E#_0j3EsHJ>$cPG{C%@y01m!aqQmjkQUB9=Q>kd?sd z0=il-%hAmqptS?VEgGvjwhUvRN~UDCBsRU--Q}S*i_#*p@t+!5yr=BF>*Tl`;}%(x zVrRw@*QK;%#G0i@kB4fOA>*=)MJxqv<;JAMSj8(bE@D3;R1d#+O;U*3l^X}zPkd6W z+AsD&pvr}c$Re|)wBQS{btq&5?VY-DCA-ws=-xbmt>3sGO>_HfA`o0fs3dlYtZ2=$ z*jimnV&S#c%8VnlrWR%^F?L#v163vz#1^g|e%g(m&W(3wETOf|GDWdQDY`L>z?R5% zmxp|~=Zqz@bv_yTvelSv3#wH1^E75nY@m(zL|-#H z;HuTk(&R~}N4HLjLoOO?Q14W3EOEU{D$L?hADO+e#7?j0*^qkWy9U^WY)fpV#MV}u zPnKAaMPy?=Nn<@mVW~x8qp>Bkwv1yvkr!`?ZJBjr7Wc{4Qw>w}EJbuS8hi9*Yy8Q* zo|=9FSUJb&Yc66nQd>IPZJx@CZ9VxQoo$)5%0_C-Fs6?6C2gKV$jqOIaa5bxN>nwSgz49QPcN2kDSG`xt%B^LVodH1G;3h3DYMmH^ zYgXLis%1MbMrg}@vh#Gsf5J0{*fyZ3W7iU`EJhZOw0O@_RB!em7Ilrj`WT6|%(liF z+xDJzCA)UHd(*5A;&qKtAh0d6o7nb@O)S(VF}BR+a0S^Vur6c|Vq0)cEm9j&jqS~a zcIbMR5iqmnHPAM>1u&`cBUJW^%%Za;vv!`4+M*Tz$sUyZvMV{3$|AJs%DS3ODI3eU z8a*((6XQ0GNsFBs=Ug&r^X%SiwAK=f)LLa#%Tmr(ExV|V%p$TGe^P>+)RH?J;ctI;D&OKnrkMq_m}tT|+Yg8T0G++1X54(gdAD`vf5 z_g>Yr7FcVodF{g+-QyuG_Szy_3QJ<_r+HgKFa#Wl`Rs2W<`+~LA%k-fQY@qUu22i4p>iwFqQZ#S&mo(onxFV?-e zE7@&8;Tk(7w)>O;D1mFgconnlKQ*yiV$BTatKE!nb+i2~6^xw>T&*Gsv-?JkCv!=E**3dFd(TyIk6|St7SAn|yr;JfF zt`S-ZY_em!#^pdQ*SPw!W_FjetC|(Awv3~)#j1xEduzZ_+1eOzw0X*Z0$Ow1EuN?> z>9O=R!0LF#eM)1NRJiNeC~VcUdjzU!rI0=Br*837ZtVUJhf>$0l$FYw)w_Qe+bEziJN{$cDxD(@4vy~mMQ&TKr z<32@cDP|?JY`+V!m`^pa$Sj2uV;tiw*RhOIXdM%al^Mr=g4~kXAruHBfu?5*htkYt^OvAzqPEC&QhdQJ$m+7zP4}J9*It9yGmU^~1=qeB+~mgX8GB-J7qh4>psqzZ zyOg%lV#X-!K*6m`SsPC(V!1?UcV6s$0V_H7pmAl!yU~+kw)VMa?_D)|+I%{<@(A3* z))P?6dxBZqCx~4`#%(?=fsM*uAiEu7Z4F3kmI0`vE!&e6rWRC@TNaM7`?3eJi&X9) zvtCJ%`+lH)F8i`Z=& zuVIS##XeeSlNoPbGiCf$Pb@A2ij25c^<`}rD>t^pIx}X-IPKZgvMFaDwa>kgUFED( z` zWA$wRc9_)?t46kI#dYF7N1P`vqIH+EmpXfhtlK=ePgJobwCzAeXQQwnD_ApS>Uh$#4u#M4KE3Al>vZAoQ>PE}S@k)$uJh7v*Po35E4H46C8(Qx0o>r-AaE-(U z*MhbUsFbr()?Utsa}cf3T8pe`Jq)ODyEN9`(^6TeMQN24@5H!8);b$nMQj`>+B|WM zV;FN>bHgUn`|dksM~w9brso&tr{vcBf9 zC!o5XRVCZoc~Z;L=GhaANUbNIJp8m}7GM{%+c0)g47L&3mf8^8N3$w!Yl&HSHL|yY z+o$|Mop0v|NLWp5!lCzySyEz)ZBwhv*q8R7LMzB3uxD?!ri`naT}sQjDJW*!F^>PF zg0{-pliHOU@2vq+*QCZZNs%+jkQs~E!ZqF5K-a@v>0Hmu#Kv21!~#GM>%`*#daWjHW7eYajVt5>TWV) zZnM&rt+W{L30R}DHjLXdMrE^a*4eS76{(HJuH1MHK)IlO*fAzC-l?&ZV}RYrmdsWy zi^^6pdsxObW$a;!(%3~TuNu&mEcI+0sGS%GSGaYbN5ooUTW6_e)8;X<4>e}1pry;x zGMgFWHlQF@>WaYDjj=VjHy_mFFW`|i>uE;-QblZ_1zc9!#pOR?7Gpu})x44zudFx) zZN?Ry8#80P+B~JQ7FtVed?=}GtvzPkDoZ`-5=xnsMHU>yyg{!Z3 zxYa!`8&4}8UU!a%|Bbx+v>&bW{fNLlJE)7Hsj1#pSwl zBN@H=OAlkuAOa#*xT3Yqt3d{pW2#w+t&?KuYg6m?&dQ6I%nDLzu8u4{+H-qrG!}lJ!6SCQ zrFk{D#A0#V;96g;v5hOYYz&~DZCrcK*j?E}W*4%jWxQlI`*|ui#x7=gFHb>>X6{ zU7iiB6?VxiIk6LC9aGD8rw8XrD-ye_*+9G9r=vA{&Qi4hWL!-wLW^Upw753(?CiKU z^BlD-3LBMeVzHpk*6iUJ2U^2wW>52J*RpLJ!|mD|V7Y}>U3-uMa(h`->tKoP$GL8A zjn+QxN9=q*B7n3;Y%fJNxIPwIN$ghIRm@@<+c+jKUiK5j#xwS=fKH6J`*e&_AhA+g zRk1E+b%$;4PLEy9uFSYL;BC*?Ba4gKqIU1U!-Be$S{!&oYTaoIEc403Pl9&&#rBNxpbRaR zG1$g1ZfYUcj&b*S46IK-E?iq>of+FRmb$joR$^Qm0}O3XKXolzp0R{=Wye*mY zS<&liR?@0fsWHsnh1<=n6;|w8Y;Tv;KJ`cJ%lr-R>5tD9TynGt&?f4fnI*b8JMu?4L~_DG9cYEPksRx^7LYmw!E z&{}4rw5!qMy#Za#qO$RetILyF3ZT6lS*+aH6Hso-9+fQBEGzC)m$IbB+k1jm6B||o zY)&UrxE8aumq)NZzQ?n4cALe57LiRY+r%QUx(})>F)K;cjgFR9aT=w)MSgK-3(w?n z`|^I%d&sPoku7Sw#nUot6Dmclk4}y0&bo%>P%F?{V=r}f0qexrVvE4i>0#M?;x>74 zA=?Uj=@+B2-ItZlc3MnAe5IasYTPP&w0VkGfm;HL#G!?DdBiM7gx*oIcj zT4Q@$u^lM1HpJ%Cs2--kF*dXdS!cy&Hg)V><-~{3hT2fuEXI05k%Iyuv=-(>xrwY*{Wv+Y^f|7+XZc_tkYtUUEY(SRXN*T9#Z4XQZ%khW7qIg%Gr!k zP{M-lSt(M!{B(BHb@-!c@FZuU^Rb4H#C$l0H3(7hRt_QCIHdQaB56a4I`%fRA4 z=_AJj{TAJQ$vJy^f4+&8teVzSXpJmMvez3&XBO89SPiUG%8jG3t1Szyc#0jQQ*@oH%8KsB$6Sf|D-Gxk75_THJ>c*gL0F-qa- zC-GVy6tuR=b~WoEyrr7;7)6AZq_`bpIy_ru zJ+rtRsEeMgV3oQG*n`>VteC9_dEZ_<=5P8teyq*m<3+&4T3^xF!qzpcQ)1HMq{eUH zKwYi|R<~w0p7Bx0?#(>dPun&25L8V<#eAYMD~avQnB17PN?Dv^H+qb0WyUUNyUjx_ z>$G^K#=QZLaSF;d$IgH&#jNe8MJ&L&nvKw=F&zm?PAu3aqqtivyoXR#&(S(vPq4Dt&`#!rT|v6dH@@pZDhA)oLLG9 zZFCl1ofxOnlMQ$PYiK?HBw{17Wj$4sr*K7Qfp#lxE39u@Wi7Is+l&`)l@+OX1uNIA zmR9ujmTD|dHy`h@I{&&O0I^^jUJ+Q=u}O)8D*~%~t+Q3mMr@@Q@qd5v#1i6^%8fx185u zwu$>#xA(+qkOkCUXsn3UyIspt$}&$;_3YAFD%zD7FRe{-T(cBrHpG@?Y?-wKwen)m zQdni#Gh2Bvn|Wx@Ug&HYP#4g)0d-6*GX4a!sO&P0t2c|nc5+;`tnK2M#;RmXWn(|V ztL>*AT5N@Fq3wD$v~CkhoJMR(j+Gme9*0%q3bA)jZ1s>fy|-9pM`u44t^UXNN@7K4 zsVvxrRZQnT zon=E0xVB$RDO(y__7jQmksMb!+lH~^7I{6SmhO($?#eE^*a@hn~m#~QnUbzXdzMct+e45 zV!ND8U$#`%J)Uawh}YUXd+g?E`$-bp%=V_)^ktnJqpoSq?$ns`wKcOvEqQUEO)=ZN z3R|x-pxomr2dX-=5!pj%D=SV~>^_fiZH;x8rwk}3#z#4;E)P=ctT+-|)RGe4wZi6z z{S;zvwZ!^~=%pv_X#_w@D~%Pk4Q{C{%tmEx84J`(jFlH(VV2}L%$isGPo6MNcNT@^ z4VA6_P9?_6Gltkz&UR*u#wIgf)hxU3#B2=XE7h!&mWwsEiS6xof|eCxNsGJ9)1Gm3 zmLr)lZxPxcOILRFWnIhS7rQOHdb5YM;hK z_ND(GhV?+!39+R%$TqQsZ0(gT%h=T{2d`uJ$;okc24Fw=dX-wAm!e$bMeHhOsbxDe zrZX#@jmDZ;ri|?zCojf?0@u!tyE98c8(N!KPTAJPiruv}V7tZ!m%NzdxR%YQMXhiJ z+-PifWyy#uE4J2()~t&%R*;5MV=HcJxzS@Gy3MBXF#Bn{u{?pIplt*gE~wkr~&pac9QcH%4PiWNjOJ_=$saVjcpmnH$G%GwXD)&A**!|+uRnkB*ql7hhrR}mAZl~3d>Ds zrO!ih9QWx0+MHssi1oIBs%4FBGGpP24aM>>MW@D;ut|-nWmC?=X?sv8EtxU=Hn&cR z$Afz5iF{H3B&_0<%frmJ_2e2>?-aA>tBTnW%P#j$i;LGdP_%lI7h@W?V;pGv%xy+C z_3RSaxKBlFkR>ISY~R+K_jyRpr#?UgM6E?u$S!JejhD#g&KtI$EVa?u z4K8*uQk&_gR@o4{lVd9^rtu;cXseWM6N=Qh#*Dj?HLSRD>v z<3Ay=_KYL3PK})uFVnb2pbA)fPZu(~OyiWa!PRrd4Q-O+@M_06jh;a3{8(a}Rj`U( zpcR|B`u^?L+7IxsoG*D?1nwDFaMhBuYJqEr-A$fMKXqld%Ei4YR3c!PVvLkr=P}C$)#cd(6&qM}W^F>5*>rkPS_Brym}?ed3$DtEUD0+W z3#JBFM==VqzFFwr{sj4P56AiV2a15i_1oxd5nI?wTfx_>fZe$p<<~4^9TjGk$T4}jWU3**0*!6ck z^1(PC{9qA)SY%e{vYZzuGv*y+R>=0HQ{r-rBehUl$ePw7mXw&x*wnVlCOJ+;o6{_k z8_RlP0P2$2)tD^<$}SYp7PEWDtfv-Xw(?>LtS#d{F9oS_Dp{w+Yx>F123ay=>#TuA zX7|f`cwX@wG_L(T!FKyk&We*5r^$0Lo9x)UZgp*BlN+1c^_afxcRtvIbH3_P5dc}Q zx(TQVEY}b#Y+GSn$XaBJ*NjrM%9h5OT2B}o+HD!5vd)Y#o-WL%0=6~QC#G2QPjq@% zRmnOrwqwkQv5AG&8d!wbba_X9Z}0gSB_eUt1lweHSKLDc4ikJ#o`k`CS}#~u|)RUW;a!A z6|#obN*k%Q&SE~5&{mtLb#^Dl5!`63r;PERm@=;VY(ZP~EGjE~ zg;^@v>hQRxmD1Ljae2m;*<{A{i-ENywuz0)-WPmx(Mo2=NxqO|5U#P%RX zpgp=g@t@i;c3*ZOi)rj)miZ?y2B6|VHL^5j*Summ#x!R=U~In_g>^NXI(BKSs#ywJ zbaq24Mvqmb_6?qK>+DNI?MHa9&OadnQQ3Q@w$UXghSw@)4ec8{HzqGGZVOt`+U0CF zX05UpywdEslvxbp_MYklP?lNm=0ReOtU&F&xE!d<;U`;A)Ug>fhT5YuyH%E?nB}3x z26i#qz;>g@)hx({TV@t}WHBOZYLgt3Aj@}x+2R&nmkEW!y2aCdo;woRrj=V~!!Hf0 zqqF}%d+z~Yd0CzRD~ce92x7$=4I0r{VvHK25_^f2SbmnMu@jBGpdg9`tgwrUv7o3} zqw6dxh>B68iM=GY*h|z1C`M_!3;(B_bKduRXJ!k#<3GFieRuBn-kF`9^4|No&vTx0 zzIV6cnMb&i+65sS#9Ce@u{`ix9$%!e)K|)^l$JtEg-w-JNgJWD!l%riLS-Erhcy zm350J6R4)Mii@SOa>XWAwpcZ-vtsG16t)R08$7{m=&W#sKgOGgCAD)_%8J{qc<9dW zKMjo)v2?__EU=w3?nPD-D_RNc1Xs>L#hLDq*_Ifyn&o0kHvBOaR>0ywXPnUL{5w{& zDr8;Dwt7})%!=74WgS4t7KhSGV>5q}G1fGNkkuh)DL|}6R<1aJ)%mi0Kw8VgzlD_G5IQCk8uCpYF7ysiDs4XV_ZmROfcjH$8e z%u;Q;mD3bck)7u|i`VGJa~ra`s709+$J38k<<20+2)HGJ=q+0(@$ZGA+X(9?wWoITWoBbF|O0x#jGn?BRh?ZLtz`Q zG{=z^&yz#m+DGo({dJ z16;@|GX|#JK!v0>m_;R<^G^<-qMps!r>teEv7{AOye6#_S>2i^VBsa^j=62{3)>p= z3lUc>2`pf3yoStr49eM)Dq0(3maumIAg)79CvlK;aq{c0N3SP6# zV`kas37J(f%W^jQvQ%0S+X`9=Emby|ZU3Enma;qdPohje%+|maiy0X&;fv{xvoQ-^OJ}3SBVNg@`aD+Hl3BZAGV7In9&zh(meg8n0V`?c0arX| zWf?1(ohOkU%w7nJsk93qmTv@CsK%8nvSO#AHDxx9F=@3k4twlUmgOvraT_xRts%0V z#x`HnkNTRW=G* zW>CO2i1nE~(dHqo0#?p=QqD$hOmu@44>P`N;AT;bMgBg-Ri<|45QqUOjJ>sP&SkJ)+sSJK!8%dEiV!9pu; z8?N%k7F*IvU>zA-U9Gg_wXIHLX^P#Nl{Yr8hSuBy*i>4j#<0fj@dUG>u~o`4EDm1% z>f-@y%b)r_kF7DHW3r0HK2H#vBa7LWZ7R#EmPKqV&>A`$LW{6i?l{`Asj?>4(~5~~ zx#Fs4liS9tK^3)w*?Hx0lfO5C#R8)|2;A1=33NkVl|iLFZn(On1*u_+EwrZAp>c96 zV_X9jV%ErNpn};`)UxXJu$awCRz03Vwl!w6H>=mB$ZD1v%jNU$`pj6ixR|BV0@`+x zd#hzFvpCWj7q)pqPj+}hXDzcHsgT0T9ZO-8SIMk0C~z9f`r5f;P)b|OMQZ14`EfXo zo3#UkHKDb_5>;7a!zyzevlKOF9L#cb5%Ma9O=hXHtuvbwi%g%|1eD8JHF==1l3Ckg z9f_-2vnjB_Y&2)H$wQ5u`r>Ac9T<1k*u7Z_te`~%C7A`WSr%tkHS z>R78Rfh}YyvVfHrEwy=?yw07XZ&a%Xg3GbRP+1LAsG3c3 zMJshS0w^i1@~4=m@bFWsW)(kKW?jqjOrC(&BT&>@jTu90_0p&zvg*rbZ#HCByau!} zvZzYdGMgvhwU?&IN>;e`#cZ7!kjT;(^N%~TsAZwG&{a}fI?Mct=s~4 zq2&e8id+M$W0P4TE2*{C(iBT%qs=ojve?=@)>s{hYbY&6){R*TZBd(%ac_)ejTJw+ zG3zFepp6q|&26272YrpuI5Oj!q{y(Cz{(jbG44|1aK!>P$0?W^o7Wmz6tO%=XVsQX znU&I}E#{PQ8#J~fhOp)$s;kK@4&CBocTTPG@W#Y&vlf7r#0pxo$|BaWaZOWLV_}Tb z8&8Va4q8>Tl^VO2<*;JOEZAjxhesfrD$6cU=1)}FoPkPf%n2w4P!!okI-95R*cs!+ zjI+msTN;0IOIF8;Gls;5$|8J9W;JB2YL;nnD6C>*h^zEfN9n95DT3K3W(}=9u~imD zEYqhJ83R<|+QI6Zp|Wzx#CNXNW<6ZTCt(K`0IiQeHx^{ow>VM3Ws$AGnEu$d*h9u- zR@N92o5mQJYGx4!`C?KledQqoC{?uX%68B)IhG|3nI)}}KcScnWScz>WGS$IQ)@N` zpxU`S7FcPlg_auIpw$_(=C=K}&=grw8-o;rwztI`eyTnXq3s$yS;{(gVq|P;O)TV9 z3cGqWzyz_amK{3FN90(`?m%cP|2~){r$*IB@JdT8c2i~fQZm(-C9OFCm1nrq59_>H zNi0QHLK{;QmRibe>TDWgb$Y0>IxWD=idS`csI$DN$PWvsx`AnSd&-xd9s>ipGPOol3EiB zY#kT}y3HH|)ojkjECogutC(e{2dq+IX^!6>%sPbvsXC{J%(|FGTI^9{aEfx4{uqn6 za`oX;GAJdWq}Q2Om$UqjBBREiEKLK*RsQ-OUW$VaA%B7tOFZF@M6sfum0y zwUAd1EViLVX)F#NeZmzjG5=^8ST%a4kuhf$vBWI1V)bTei6yedEIqLl)<@tWo&EnlsPuCu4O?hK*gnaHMiO5A-Z#hYW=!f z_NblH&jz5tm4CQ=q{`~8Q(Agr{;|xa(lUg~>`6KcUVEiAux8d$D^w$ZLW^fo%SLJ( ztyxH{L^gPB&Y0kKU7i3|%~^S4xMG1zW)oTY;#Ao@mq$~^5gLcesxiy|&poCoEVAU4 z+`5{bG-j)kmD*Zm!K_p^#g--$7hKxlk3tE6nfmH+*)JkBfv0mYfNvyG@E$(HuGsa*xYgv0^*j(_){rum9yQLG3Sh_w8Ry_N@xjev}R?BC9s-(>V!6$JVrJY zme96I3L=|%F_|@~#MN&xtO#HSvwkR!9DmnK6r54H*+zHhFRx*|5gcSSC;e7N~+$FWjzWVCNp% zjXze$-!2eTk&0sxOI|6lI#6%J5~t1r(co1on^Nlpstqi5GOJpa4O*s84PAD55?f}* zkv%bi(yCF4Xz>`^tY$-Ho5W^Y)&lD$4-?}oX9L-I0VqVrtYmd`87q;c&T{@qGFy#V z5}Rk>aq=ltw$i5rwwRqvKM~koW(8`8t7x6y)TwN-ZVW0nV+RZ?nJsF?Y#3xho3y6R z_Q-OKqWU~=#a(Lbc?#CD*_q9_SZW)tI9`;(U7j3$qQsJ0m$Gj1bkk4Z)o+qo6|z1x zAa#}^o3uKBBC*FEr$Tn}x^3+5=#Q>Fu1H|vi5VE{5ItE^8~Ia4P(iI*JTYq=N}JZ0 zz=Bw1P+dX0s;RAT^*F`!@jH+2ZN?*ZzCQ&ji=`ttjn&n$;?>l4+_J}$uO=g2`x66f zjy?UPqmgy~l+~;SwnR3&vg-3hm#2@PSkEdlZkch*piFFXJB1b+tISx~sxhn9Y$e7O z8b@a~6t+Z`^(-y1kE~`1tA&=9n8rA{zh;y6*5^J5!O4gGUw#FGhWoNdUJgj4Fi!HR3KLxRxr)Zs7)v}?q;f*7H zVm+&BHUndOW7%SYOIUeTONlYfu^Y2u7Pkdz=&Qh$zOL?}4Or*ClpV}&99R6U-7t}z zaBF5Iu98{63XLt9Wi6W`%Qg?KaZo#BjH5LxV#RAfE0I-~N5yOljJr_^I^*!gF-XC_ zEUDGXMvu;zWm@c7)~T^na%1U3iG?SYE!NF7jEqZSX^#!8LMUi#g-?K$#yA2f z8DlfcI@Y$>6N??QGR6W{%=Yn9sw`pEMgBO5&6j_Q{E4ps13EpZKZtjKsutcynnZ#Y!R&Ip`r`}K*Sz3nE(>jSXQ{Ll zSS+y0L-5**te9rfSk5@| zVhtE)s|WHL8mrV;)G9T0atxI%k##NW^eF=6qUA!O`#299X=UZcV|KjXS?U9 zLFIBgAZtuw@mA2f!J|Sppp`MEx@yhnERAtQ##PBy_LR<;)oib_j-SA6P%8gmb zsyjI&!Vs;H(aT;SvY^SoJuws@n%k(KCs5cZ4?kvk=)#jWaRM zCJ$)M&a6(ro7S=npHgHer}GF`Sz{5anMK#Kpp?imE#^VAvMn32@l0gwIb#n~WctMD zDK%ETS-IkgH7=nww;{2>R&lY4*^xr!v(;d8`5jo0zV<4s1d6G#a^qCm4%&t+BV(U| z*Y{?f8fO`6Vhh?h$=y0z@e?#wp|MSIl(RVkmFKuiWurCA{E67+X#t9$A~t3<8_cH2 zR+C3{tg$7nZ2(F_+nvfITuWxl8OP~&cE*mNQfq08v(pp3Ss}|pmO9%vXCbi!7rYYL zBvth0gLclZmw&|0@!!xEuO+MERf0=_)kVApt-7Kow$fT;y9mn6DlKLyE0I(&Tmadp9GUEu0vzRrwW){dQf+DOD zKxKjco%^pq0FuS(dYuS>;bW+8Qf^ z(jiZD&xu9G#X0_DiB)H|ptUjfIqpze$!w*@X!FP)(--q?siGF|Qn0ER|&h<;WQ0ia7v9 zWCg1tV=_DQS~59KQL_|q#<`MM4lK3+Dv0H|JlW)d(lUFBaf<57vXJ#KMP5bx1X#7m zDsC859#zOHGv7PHY4rWjYM zWs6w{P#v!^uh?+a%woVwY_SwT`9PITfpsCv#MqT=KnuAgtg&c~`4}An%Ux(|@gsK5 zKLTy#4vjKvX7j{=?D7a#J7cC#A{G*h8~tk;voBjR z+nqAohsGK-Ryj*v+2+ytcQnWfjoF!1pGV1)4*@PP?RW*Pt9fbA@-eDTPn5ImF+1HCAM-Q|~H(vMu(!qNO%Y zzH|2EVG8(SrN&v!s?7sXUCz3mEozA?R+?ivV&a;}Dl`VDvBWEpT@7{R60^J-%+5SX zE%Bh8=MNE40*fP2)qyvZJjoj4fHIb}wl)tHRw^r6dB{!=d$QT(A+XGi165psS6uegUra-r7UIE@>(N{>5Bu|2%r*J5W8By7PHQb^=JjS zMtJ72JFoA~GoZ`cIreexZdRrbXEsT+WjzM_0Ba3Qaf*(zy00hR7})hej#YF?E!NT%O>yPMw#6JXWQ^oclsSv)`77U zHkv$1EB&!dano2uPuAIBmf*UKO@BN!#vm1cI$SNWW;P$E5CRVk~YR)$!0Y^Sh3Bf!W`TC;(yOtFI}9;LG^WqXB{&{jPg0hDpo5vUSc{byQ5 zZHcVXC;H;hS>;b2GmcRT%~C`yE1?y#%AYcRYMF65W8+G4S;o>8i`S%8#jL7X7PFBT zTWE!9$7`29QD`BlLUs|jLtGoD{jE7BbBSNwoKk1SE6K%@Ck}8yEJapFG0P^8RW|F{ zHd3KF)~;Ams{pFHJYkLnY*9;tjA;rsda(3LG7wp&PZ2>efQlwh;R=;aV%6p02vj%y zL|2T9N?D&8pmH|7F|TcmV*rXxo~&gM7gUo5p1v|?7+HgD_(&(s=AW{oZM)hq09 zcSWhiA1%gDtYx+GI^G>uW2T~kOJ2#UaHYb=k}c+Kf@)ybIDi7YS~SE8j&YTzxH~U) zZx$78Aj{C$S+PRn42;$0DW%P}tciun${8y(w$@r@Q)q3A&8z|_>#V%7tuZy0%qF&) zeo9~LiA8(kENI&-g~KOGtYlWiN@>#_x9BNSV@a)LHVay`cVv%)S6y|ut_G~8wWww2 zG@|0=o}b!dcJ6O2kXpjo0gLz4m*usWlBc4T&R8X_xUEBx8>1BYnyCLA#D+JfF^>F+ z<~WMkR?1py>5VC^UO?3=M=2z-ENHb@$a3YpqOvCdORcRgk27QGEBid*j5CAkRF=hT zDs5=&G=7TI*t~`_PMM8z){!wmWt+$IiVmNYK!Hv2%PVurqj(+AE(7h{lRDV|D=nby zv2{V)n58$ypU_y1QKT)-CeQoPlVxVi)EHMfnaAOinmiq{kv|F7V3zYwqP4OoL7Uvt z8Yi`K$H=+iW5-MSVD_srFCCc`4eq% zAS;;-VmWS1apeKL64`!LW0nc>ShG7Y=a??>>jTh*)Y+O=G_cB^n#M+-hmKf63u0Si zwkH;Pn@yhH9E(_`PinyAJ z{t^$``TURsHfdcerJbo{+24U7o?*5-FF?q`6X&8g=Ca7LIV)%EyjX3X456g5*4d1V zjjRWtEVQa;Lt;5)tX2=njado?P)v-mSk2-MxI$=!Y#UibG3z#u2Ntu<6N41-8jG5z zU}sj)Rw+wj#jQeP=Eb42aK;u|9R12qHD?@?6rNek!Kc#MUS27xc%KhV-pYsQxSrX8 z`2=*)Zw+Aur}}V-EC80yrYp`2sySm2%nZuaEPZj4Sr@Vf*5I1g1hx;Klt0lDcas#7 z*%+meInHvnQ`(rP0H`Qt^*;s#vZOY>ajC2Z7Il!&s=)(bqn>ps%aO&BSmGKw%N9=* zw9`IM8e?8p_SDT%M31Lw?et_GDlGn#H>Sv%Vskt5U>)yj2Npeg=kx7E@+iPS!J`5HM5aEbWl+6CP3Lh!`& zDOOEVB(}u00w_Tn;SUok!_Y0-G;p z9LCtpLS3b^WLB9mlc%;)XG>*S&zf5J;-Iz_vQXG?#x+mDyf`aaLo1n89V?wpsbxLe zLz|VX6DY+`B$g5jk!`$o=6Lx>?D!i$T<891UK_1uHWK4vwvAFaI1Y`K%1)$K6BR`* z!Oc}|9w){zO`%d&z-F7L3R!T=2r8D!S)DUWT(u&CVi_9?z!F+lvaDu3vY6-dWSb{4 zV-8X%eFC;BW5XPSSvq43Qb5E-_U;#$}E2#}!X^mrOF}0S+;$t$#JVIGClv@)C=<8d))GTYLgFmX)ll*&vqTlTu?AvMOaWF3tohWtQ&vIF_-JSR8c9 zYy`&Xj!A2jvQ$}EV>@F&8$z4&iiDQLN@rEkmc*7N4px;rO`s*L6Pd*ajP1rAvUC4; zgIVqXWtPZxi?-O*T4qISpBXcJ0=EvJ8nq#@cE@>MK&P`t){dC05?CXvf>zAR8K=@p zWZ9T)wJby9T!PjClw?*CYi}IwS*WZemIoSRPgAr~R<@Xj*5hg8PpV`EtOH{sD`HX0 zQf0}jVC6A}7R_xoX05K)Sr)X=S}uqy7ZtLVwP(<5t)12tC0m?Dc6GTZ7|(rQ~wYK^R1aW#4v zJ5gvOeM+enta$*hjaD0D>a30NKz2D~=l*pU&1-{J*hcge(Q)c*%_~Y@l|N0G&DWC= zv^c~{aQVU%pcWUNFq_L~xl3q)YZ~KpT49e@XExeAIZI(~ zXJl5@tevrxwu;$IpsLBkaf)0dHaoMF*`U^w6z^kSF~lL#}k3E8$D?Agvcs=iU=x4DddYKw2X{HW95rg(W;=Wu59#q=!{cl zvxp_KfK`8R=og6{61xdr`PcE7o$D(IEdPjC`r%lbsgN;VLtCuCSj=WqR!VDHskBke z_O3XmDeQ`)mX*Y&JEqQBXp_Yf zry#7Mu?ALJ3wdQtE0wME377)g42@eg+fNz0ztfW&$Rc=xzLvxi+n|>C=IV(Z>=Mpp z)?CBNUmmQISS(y?xx+IFjm>L#V+FTwjb&dPFHFH| z782VJRCFpE)ol7=q)&OyEEKjx7Q8}dW5_tAHbfTYn6ppPTF1tPEFE!{vVHnwa>E*D zJ=^)>&{thKGj6ROHfJrfE@dk)2Cw;+yxI?g)B)=6Rp z#T3~#Pa&E0nE|%MKKjIfkXV^x&KQH;X!5w(Lxt6)b!92989*_6f+LnYCa7I#ELdmG z3~-B<$Tn{bV4en@QL*HO4~g~$Ia;u@KHOb3!;kO+!q`d5?KI>zap_NWyvdr zR$@zW)g{J^1#Apd7}p4l3GBogLu6B6iS0Cf(toIxGV9dXC*ZlBb^gS>I9xHJCwF-` zt!R;zIkqn*wOlG=gIKAnlP3bJi3&1Ha}13Qa0zS;Ky^dLgfwL~`#eN8d$Z)1RcvO) zOpT?ouMSV#rBAB?EOE`Fm?f@**(saLy*H=4^4A8ZIC$kXSf$E_#>N#jR)2z8+2Tr# z)sWYz#Hf|AB2u@+f~E5MZ{*3r`xu45TL zQDiksL4S0=<9dl5m}_1EDi*hZN53ahJ6uI9d1YkW1}O^J$s|SV z^H64mEXYlfl{pS#HM3ZqS*B0Xmo>TU@knGRU0H|5s%MkhC}Rcfgj$^$AYUwCi`Ov4 zS;|gMn3XSfUR;53N^Ck~L2HH802B_A*jC9#@+6gYB`b2B8w*w-t2ctG3m=#g+@Z2d zFgp7oK49l{30F#N;Yw^}jzOwdl(JM=ES9po$_0ImS~gTxS}U2Q&{}LYPvL5ozzW(9 zSq(qAHJjs4)#70gRpS&Pvb4of%f@K|tYn+e=E(tU%92@5Qb1y7RFKr(#ceuc>#S{Ym}07|xFxmiv2dj;#$o`K)V8yEYR1^5EMzv~ zVq!~I3}Vw7GkT)Vx6%3k#H2J!SET zcexxDmo=s%7O`QBNvj<)l(u?2N}oz$6IwId8DsT%LS<3SCbc>LggUk=S{Y;fpOunX zTnSdPN?McHh>YL*R?Jd_EmmK)u&wbY8e`}z&kWF8w0OuWh^@}7Lu2Joo-(#UhQg*X zc1cT_oz$|>+mzWTW!ampMO;fefxF)Lrap1c7&qnjDv!GR|>S&cEw#BUT;x*>h z#yD*;uZvoR#)^$etRrJ)P_)Hz#Xhug3h1kZR`ZIgW@DBD*aBDv$GOy(bpmB*8@AIf z54nZD8d*wgb$P-UYs%O{n}M-j{wYM3a}|*p(;Y8S$Wmdo)R!f=Vm0N}0hH3G;uSY0 z=6DLMp_RVMAP=S89CGKE*`U?7Sma7)L2N@-DjQPkLUzU(GcneRSqdX7Y&Av!VyU!R zXv)SsMG{MAta8=~R0yqMCAAEoa{S5mSkzWE>$BWLWlL)1i+MM7BLB$E}U-z`Qa`SaDgK%$nF}&W6xN{-pB) z64;8LG_YuH$u8Y-ifnaxn#Q`rqakArKxq&vrYS;XW6T&TD`Tu$*36=sb^K(3O_^0{ z>;y{nY&2!x!k2$ak)_mTW?bVGsA8izOO-8WOkllQ{?y5=&*hQQvLl=M6YcQ?+s+n8 zUp9~xuK*U<_KRw}(TC{x%b6XRS7!ZtOJ{Y^@DnpA!RydiDys=7H+d!nEhV;)rOcXG zifp7$ENHuG*2QdztbB2ueg|M9fHJWl7ZRJk7;&*xR-tiCEh;ju&^VAKuk6i=RstJc zp01b$u*s}lv5pC>jd2vT89vDtgI6OfUJWfpwse-kQ!lfOo>F5eu%y**hE})^U}uor zENAm<9_7W% zpEzRdSqca(ProCyZ1P0>q-hFM>ngSqsC)@16tb*kMXia|Ie5Ue$!ufRi81TgB6gy( z%AY*@lmiv$%{HN>A$C2hHcuk!%s9=lym8oK0o#yWEog;o2`wI;YVnZU!R{tujZc_E zW`!&O)vvxlaDXCl)EPSH^66-}{ z%tdI`mMxj>C6?SmS9yj&!&V|oY6)#7P_AVMv71ZmeD-HMofWS{R@f5UZbc2-t|YEL z$Qny$Jx$T9u`Drh?Tj&Ct;o0=rcn4)<*WrZRW@2YZ1VVQ9$4b2WTmr4wv9ir(W73E zoG}lDYeMU=SVt0z71o&flLM$Q#>$MTv7xgN*fhpWpC+qGteI_%Suty5!Kr~QiKRP+ zxDHk~pVj=#In-JF$_pv4yy<0DC)KP~Higzo%jiiL^P1+^#cY(aq*cH+YNfEimd-e# zt=KpMW6eILGghA`xvh~3)2d_WtRxn^64=z)YRooj^W;1EVt09nY^iJ!%T`Yi+iWo{ zv28J>w#84bWK(B7Mv=%G*g6tX39Q(~O?u%V)=Ha?$Z@@?9hgIA!6<)g9uwWdwsdxl z=O{{JrLJ9Q9JMT>r;3Zotwh#GH)UPP_OY>{WoF#Vtfna1#Ny({tZlK4arI@D7n9hg zu}YvE7iatw4Ia-eqL@WVt9eBe%j_v-HqV%)F;*=b85B=)w=?dL^^mb6D2_jQ_DMHn zj8)beTEWFLq}(+%Z(PW)>T>E@TPqG%_}+ z%^FWK<3a6|%8kD_XUtMzDXwuyeRYqgL>B1M7jtqk)~w9>yjitnn=Q7^hRPDyNT9lr z#TcldEr!feX+31@(AeV?o`Wi4-JQ+wiGvitRn;u#jH$G7x~!~m_+lkb6+eO7YR(c} z+2X#Oh0ucC_J0QOWS+^Gu|-zFQ`NH|*3?p3DYMgMg{`BRt)q-FbEu)R8wYfWznw2; z0V`?c0he48jrHUc1((?3!a7!=vACtcQeWkb)#I_$l2_1bU|Su_77rxWD(hNSeIDy9 zj$E#1JxgJs&B!?OC$)K;K#|$>#w=ztGLB-FiLqO=l-Z=Vy^;)h)oPtt2`wG6RF+CB zWEmUR_)|Dzl(K~^gQ)1u!W9Rzja&xC%8Xap6+>Zt1K0{x*RzA!C7|5Ihvth}{kGKE z8D@KhEq~mrthgn#!Zo<1C!Vcl0d3!xC9;B+)b;^XRkQ7Ep6&!ZwR)65nN~)|KsKrE zWHxJABMVwZEMVY5+KyJ-%n*vxPk@z|nxv4*PMtB! z*bJdso5zzBl379Fo5iwSKgtm;`HtN-OgmYA~?nx|j@#f2k@t#Wo& zWR(`%7iV5vt)7H7GGp3enPcf}WyTOzsB8&sKmRmo^8n9DExQWDuHu_6Qdv9zt+OM6 zYAkNa4Yv4Ii#Su=inek8{JUDp(QQQ!8Er*RaOMHWFjX zYvfQ-%w`BhU`cK+Ib%goR9UA`%AU-uT(RS)4%n{816v#;P&rOflN6z|L>7w*OM%rD z46&(|Jtnoe2rL(1jm2uV;wP!Bym1gqp=JK0MQ?0htb&$hZJ`=BN@Ss}QOmXf$}%fx z0qAO0vxTgfO=<_Qn+t09d2_=oe^GHOSB#q=70bnJnqw)gtuY;PEYQj&XG~6A&2|Dy zZZ%EO4jDa#%2HoRES5U65LoJ~RQ8>iGS;a9%8c9TJY+WO*@&N7U~FQOS5~yrSToxN zP(E!I)DqaR#(W9#3ZUqVwUSp{tDd#WLShZ9sBP|eqOmr{NS_oID=kiI3}Q`fV;0Z? zRV-W&iS1b2+?#X5EO}i^Y5^(_yjCSkPb_+EjlpSi#*UtBkKN*_^og8OW;IH|tE3jR zdYA$?P{#5?UOoN0CQqAw;yJS}XE9=&0TibxP|Ch7Y%z7#mY8yz&`M_e^8!3g zQO=lov4~~-v^20ewWvzARMzuPa>jJWT9k6aL zpi6w79?A1?AhQ6KxcU&xl3Zeog<95E85s*!rN+|Prm?&QUg?ayL854w$bX;n7As0V)~T%lWD~=N03Tr`Q{~g1*8O49$Bcmko7T(Sb&0Vum-wRu>=Vo7B!vM~WAW8CBFK2JOSjv^Z}OKu^woP6RN zvtuf(xTP%ytx?NHL0iyvC2hq|PM%sJE17jQOImRuu*t1TSv)N5iH9%VEJ7E5k{{3W zaHPCaXQi~H*7|CV6|)xENS|2CasY~rSu>ldF;L~3{t((U#f+Y4i^CZM*cLyrmQ_95 zs#)QxL#9tq*kCs2pR&zEfu%Qg0HxL}jj{e;9@es45k7UpPhI{Lm8|MmNB9`t z@fT3l8@MH{T3sGu* z0c-}w4xp-zl{W^kj-RBjI=G(I$fA+eE6Ipj#80ebMJs{jMdt)mVqCshrL15Tw172M zC$mhTrfu2I8H3eIpnz!-OK59ZX@}M>W^r>rG6&4!R|Kj)PMKvz>rz(v6KU<7afMH8 z^K{iLkrlJ#HUcR5V_9R_W0telS7Z@i7@SIH zoj=hScTHK|bOIH%?1WebP<)HL*tKkQdLn;v0F^4MVPl6-6Q%8tjaNpka+bi#8!tNh z4&=fWD>){s3ZHlbD^GO~Uu;|4S!0;uC}-7{O`U}==A5y0cA6MNa3QjI!yjG~)F3r( zE`BJEk8I`+?9jN@F9&W-E3RwV8=F@g*U%CRS};p&9X+|rW2vRuI)5U$a>jr(pmm=o z%yG!9awiBaq!tpZa@JBSfsGE2CDwh}sAVOx-D&O`GnUQ@R~@RFbz2q%EnrQVl{I!b z%fJ|Kd}WH(=_zE@SSrgx*2t1swRkMH#1#vq>I%4W zDS1k2t+ku^2pyj`y94#G-obBx)DBm2Td>v*A!};mWF7)5T$vfSmBiwjLThNl8P6n8 zaKtf9;l%h|8h+{;vm#c;nElz#6SI;<`XqtP_z87vYHY;D5g1En$*eoG6+Wq!?aSFr zpj^y?REn%loF%pH@{rjnw5hUD%t~lYERS9qH8!ZG72Im+3YcQ?(Z?(P8SBFCzz&Wo zem&BPBd+}pd1V=^x8halQ&ZU)W@ii9>dmIgN@T+rvpd_>v#MlW%w|FBnMD_~W|l8z zY+sB*wn|wIKnYtld4Mac*;L&HvS}MTz#GxfeK`KMOJlK4Njr7cEy0S)`fkTj(^wf03OFX_)1jQf?EQt1y*@I zMb@5Jwph~?<&GIaS!v~rnL$xvp|HAQ70X7CN?FEFN{yq}BYPa&w)1(&E3+q`79e6f zbIg+hd}4rv*5~rPl^RQ8gW3v>89@PD*0DUMEmp6Gz?#@_$0C-q6q88`GD}|^Ity4+ zXtR)IS`1I@H`lQ)WUaFj*}#?L3Rqz)T$MV>A6sj?2jIB5#XGQr1Iu5}s0!DDR*A7D zpaR=9eJQJAR!tr{j#RjQQG6l36=rMaRHZc`@^+^v0@X z`*M~At&(Gcs!NKj5-7&T=5|#nZ1`grwBVQCc#9vT;~z|RV25WGrWm*;xf0p5#huJD zeu@i2V+}y5md!&JvqF{!ma@D^WUaA8)_qwvdRWJ@&y%ARM%K^dvaa<0g+pw#$s|ifN6R7yC$+ zC9laWq3!xSNvn_ruqCw6S}%oAeCS0GHF*?{ceZ2)=73r9+EdGpY-fuBt5@d6u*5yK zoz5aFhR)g%x+{HVvgEK2%Ok(wi0?Upp1y(n3jLD~&VTxvqS;$6eEM#j0 zish_jHaxLpmf@4laq8?$EgQ{QTH}n25f+=->G%_L7H^X_&!PaTsca{+;ffotU6V&6 zP#~741=LCI9#`a%m8^Kp0IIKNQ)Xd^vnw0kIC%xGB$Y>A@!-H1>@q6mVOZnE6mHo^ zc5G%zYAi@=EJntGECqHAspX%lXeG26Kqas;$4zD_t14$xUzI=+S2C+=7PdG?pft89 ziKQ#%g{R(iG8>^Wp=D~^M3!d;urWJZ%EBEhdn#{?2r7$NHh8$g7n4}qVkA$_pfpH< zYStP{Zyd!enFX#Q7RI=Iaam$;3ytM+MO)Ak*M5=MSYTDW`jI)P-7-QKe|A2kcg$-L zyC80fD{h+Byh=-Kaw|2S7~`h0%%38BQaQ^?wy5>QBCA>Ud0frLnLHUl(H4W&Ore5X zcV<0GL0+TJ!`RsMY&v5H#woPzv{^;Qg)66@_@_r!*>OV4#8{!RfF-jIi+x36yTq8x z=0HW~jFZ;1#yzTeTgV#QOq~Gh(AdQUZuv)cOjhf+60|;$*i=}W<672PBg?)lC!o?B zD>qK5tq~|CP+j~)jiocD$cosApk#|ZOCe$nEoimQmN!;0n;J`6vy^rIyXTW>wLK#!_GqKcY{x>i|80ycKO$G$oN)mgO1s8|teCaR2CKeGXeYjyxK^8|Q&}mkV9hR%&%UF= zW+~gLtXe#V7Q}`#c8e#RaXZi5<4;qSjW$osEH+{l8Os&J4O?Y7{se_}WLzq%O17%m ze$u#XaVjmVST3k5E`qj`*qMn6m}7Gbc};;G#BL#-OMiO*DBtmJLv97E4|K*}bp!9x z4eLOO#UiO?jcWpmgA|^pkTEv1QrOT~W>0`MR;nz+r)bRTq*-!HW+$sDEriyUtcm3) z1yzs}u8e`5=Xt9!YDa-gNjB%(eyF5|KqKH)nRRL7=c|vAGWhbR9O>x)gF|bxz zws}0YC{w(OtV&*Wq#p*U!x=A4--Y|=j>jo~BU)q9ia&e@*Wk6@D70Kr$SN}yu0gEy zH4mPG%JfOta+tz8TVKwt1kiUH;TmmLnA;wjoPsQ)nYC zMlFk#=9v1brS^_HJlW<^F&k}Jk5c4Wvr<{*Pd3K(#w3=@6|Hy$us{|EEFE~GdOO1z z_uy^C?H!O;{Yv1KA`3osAgI042CjmY&{AdzY5}VNs)!}Be8=3jN|w0BsR2~l=*(vN zBwNgbR?HYfVtZ(#m}M>N1d8!flUN}uUo3AdV6C$bi~($%8DMuDaWSsIEKixu)@;_X zAXa@@8Dr`zPYsAASM1g-6qdbNRkA)>Xr-_=#nRXbv8I(eJ6STv1K6#AbmpJ#sLcBJ zqTGU75ld*f!Wipz>8qFpwM}C4OdfM9X6=Y2wVb5Dk|p+m)od2D9Hyvo3Z5K*6`k1* zSI(L%F_biimRHnz*1_ln#9hQvcgu=6w&L^ zps|_7v3P~7&Q3s)T3lkSg1#oQEMpVZc5CMGH{RXy9oP|>C8R4TL{+$A2{Qq}FEKM<{pm3ZxV`^+UW9zJ|*_>B2wUI!{7~3LCW{X%TtVGs2 z>pqY4md4nBbjI|?JP_Kh(W4Ql$c)*Ur88z$ ztg9xp^u@e5c*0e>V=@a^bxdXfDySv4JdU-Ec3?+j)}TUV4K5XyQxqk$L2X1&Qd!rt zsj_@o3IqFo^>{*RC9vJ;cfB{J&{m(v5-W`*x1iKrrmYvz?@yHibZYi>r8_N~5 zoF%mG&GJRa$*d$cTC>14La5YO(yIVU-VN?y0oV|9EP@4$}7ELmL(TB);Kz5=JXJyXppHdal`)~wvI zl$NzDtufxrtlTk7alEOPmD0MRodMR~*kvq^B$nKAC9-btm|DffbjY^GxYp!T;c8;L z6Yz9)0G)CEQU1ixm~||RSwV}m*r{=PV-ahemBcE3a%@as16hSnA+*9anWerGS+Bl| zT}^!jucFn^^3i2nDYkmY54dsM>K)htnI*A?*6hj`16YY|IpgreHpaBYx-dJYEf%!0 z#(`^DW9qD1J()eRoK1~wXYz1l(TOoCStBc3Os(a)cRWaFA+uqN-I`S`J2{&N(6+#s zr??YZCs5K@pE8>|D~+|#W{;=OpL_-$U9tbLn6<>l$pO?@EP3NF#}HiMn!F~pP}!!n z5Z9SSR!d{+##TRM=l%=qSj>Xe8D62XnLJTy;}W@(dpx3b(&nKtb}h>UiUE|6O;{zf zAeAL;v&3@7l|B`*l^9!QZH@K16lS(wnZi9DiY)(l^_^Kd5j2pD>$~JSns$^E+W+6L#@d`Y{>{!f- zSR81G@s>-l7OyF_MXZWh%4~>i600^(CB|Wl!xyubbv+wZEUqT;6E)UNo-oGl%LcQQ zSjj9^Rx+!+7!_>{KbhLBV{u);wi5$117%|4{JU_)S8D2RYC2=PV`58ZER`JsyREU^!I-5f#;=sH@|saGuPCzQ zHkO(^q&0nUI|0ucOQGc;MT#t>R@fTaOpQrwv&N9wEN1PC!7JZX%d(s`xUj~VKqax= z$YS_nhEG^Ju+L+I41En>j9bp1s>9POtyMPWpF(7*wBS_-I%B3!ii;_=ybxDZvhv16 zwusH-DQj7K;?5T*ud7WhVO2FNYzMX5mf9VPS>kGMtQ#Gn6*<%@FXl!Q2F-#H2N@WGC5@S=F0aRqhqBbQ~z*1zJIkq#F+(L1=psc|w zZea;ok-Kdluj8v7eKa53JJJHS3y@hVEiaR5wj_29M3%rBS~+5)%jgL!c%{f@XO=3< zrK4)u95c4k(i(I8iHq?QfVDFwtI%1^Qt*oD6E8umXN;K`GcOjjU2L4a*;dH1)1z9p zjX$xZEs50x6uUh3#XPEzr7y;6jadWBb7mbGQ)fG0Olqq)n*mgnvq3A2@v2q0@W_K# zU?rU$h^@d5>`1)gcL`7lYc23u`bumCY^G2WT()^qXSj%%%Ah)6rLs+Eg)G&!f9WT>VlfL>tkVJjE#)5MBbraKOmWnxT$)uHKZ#7~_sZpentW+}_qShQwqHVfHQ zSd_C~4xkEJ#!pV6%&NJ~BMmVYsLIP+1GWOc{0_yeei=xus0FB85ZM5hx23a;pMb6< zmICWHqH0^L_=)+G+;N6a1Xunz6Js`Jv2^IijKdoTw4t%qSrSWZNvjIk2#ww733Hr% zo^Jlhr7Un2vlxBiGO`gt<%=xR7W+-UcuFj5Stu+!vtIJWvc(n3c-P9jlyW zkEc}D6HtIQnmlUEYW#@`TLVxkYFWo}DSVUEzA5VG*-NJsj)KSq*e7SYF95H8_*KgTq2etJ5+Wh zo@jO$W>w3&pk0`lV=`MhyT-n_8RMQ>&{~ZiD{LlEa>c?nX>H6xWIap~(7MNiV>>f| zsd4fe)hvOPIhMrQ8mGw08(Ux*J~4y>tyRmSD+^$GabS$1R{zI`B4ZNUF`EIDri`O8 zOK%+JxD(kT7T`i&t2N6OPsl7hF@3Q|Dw5e0+VaR@jDy=j?Y5_OM`1Q-h0ao6(;JJ` zMl3wB-*nBvr_@==EiJJNTHb;iZpd<)f(xkX=-jye#{hDh-B~gVp=E=I8ml_Cof^Pu zmJ90YQkKl-s=yew*l(JCBC@o_6+i{E$e*S?9s^5cl^JKFMGOJ3qFJ=`& z5nP#K;o6W*UV(1;;v9Yok#z(`T&c7yXR%nz${W}8lSMXl*2t1t%WP}RGA>qRtSRFt zXQi{&*eqt<=aJ6lc>$foRxO(nD|Jm`t*|)C6lY+}Ns2y!niR7T);@v)q>Ws?g9EWu z5$i&hwwRCALF@_;yTi;>NM@5-Qksj2Qwc4NG2b#V?v1g0aYL3O+q$#TSRcw9GcQhR zt+OQ7@slLhDodd)nJr+`88d$JIe66Cd@~l`YnINKQtQAtwYE(x5?#l|;x$!P*t()6x5PF}Sr)Q% z#<;MQ?Wm1vw&sl6**rwnGf)v3xA;lG3R$I3OE4QROrgv;11L6nSk98zNnorwV~0@S zG&{58v|-CyHnB~G1+;C^4^wG*9BajPU`JpU$Z8Q%k;@xcAk;^}Dv5Po9HyAOrqrsE zbpi!ItCCGvOK4MOyEA4xUMn!pViuGFS~huzEM%6{W+6+7?N^nvkw1mV5?W|0FF+Qo z+8i5N6}0i+%&N!JRkDT_jP{)#0n4y>;*PU3%W*}W%|nI760<~=3!J9H(hui_%%(eL z87pTzpj|O!xBq&!fVIq$Rj=~I5gMm4?wv7-ol#oGPo7zfiZ&-HB71WBWR-=~Dl%@= z+8aY%)#JgnmYEf|1U5@q`r>fLJki~?EbCZyW~r|_kl3`wMJs`gV%C{4bv9p?!ow68 zG^RDCu0}P>dBsp!3T?kYDn+&_tdSM9)tqG=+k;DLi7ugy<>ZNCyK)cNx&O)`QQPko znH9HzYF!nxQdtL3lvy*&#F(M6iPgCQrqzM5juc$EW2r0yr~)=mzaz3V$D&rXY)Eb4 zTIFmfvrTDp{)ypJddAyCcn)XOKT&1>RPiY zvoOUx$6arUEp@h}mb8M`Vpg`;Kn-G7@`+{pg|&WiP%3VDjY}*U<8ARJFaDkW%?w09O0AAaf3FC z*-DJbtO8>(3vzXmd)Bhh*KEx)f6B4NI+w>Ho6vFs>R7+-lZx5w_Ea%TXH0ApSZb_v zmh!3vaqUHxT5E|N%noLY&t+~1TEiOHI)P#YWm0tnu$3CKl1-fzuq`w8^pmKK42nXl zN)|_i#lA>jfvMX(7TGLlQ)atjwmUOG?wGvN8q*Z>Evi{NW6n|7AIlb3Xe^ z$WAlkNT4LGsk0iTa6#K5C^vdo$6`&pJT7Qe%K}y#F<1w(xj7w;xuO+gKpju`;>CL)53y}q@23N*7Qsa1C3d(FY zW=mrMZbeXV#;LO$reJSYr}8+3BCo7x`zB8sV-~aU##(`F53RTbuBc{fl%msEG3!G= zOED>DjjNFD3R>A>8subFuGruLSO3wKU)Udp)QbikTYg{F@gWHwmc4-i{laDTlR0G>#E1dvJcuh|LWTbolF0iWOgn6Fb}!{snfo!nUyV0YF)_&v7(ph zQv^`z%t~mR#PaMrxMCrjec6CEMix1(nE6x4EIT`uJ_WGKiw$gqPyBB_B(EYC$g<7D zMTzC&l(DUGv}JjgJMv;r8p9HkS)DMO$SQhr0L3nkmn7B)Rk9wa0JB&$#joWQ1uwNI ztqh^4wThm21z@?r>gqr>Zv;m4#skS`3EYl}hSc?wzxQn1*lr9 z!0aku%OkJ2B(kD)RJ1Eh?Djryyda?kv}?&OuDwWW4lUXe6WS2l@WeLAl2`J|%Y@c2 z#c={&gioATbi}mAxFWOSmEIWW(inr*Skl=fR^U=*sja$F6&ryuu5w9b zjjDL{f%+x0t#=2N*aTKr2pRhb+46$YW zB#HH*BI5+Mqqdc@JOfW}EM{}ixYe@#1QbEls@7~}#;uf1TbzRwntlpftYHcjvpmb4 zV~eU{Wr}&|PI4!-6LU;ztHmR+oyu%j;$W6-*>04A=t5!}wCwZnGGJY4SmWz2gTnUl zVXA6sY%j6|wxCsDoSj*SY@1djwGlosE7qlpj9Wpg)+~K77JIV-RstI;o2T=@6K80g z2~=0gLS##4HA}%-*6x@h+vzM|)xct9#xYCbS~dh$z8Jg;S2Ig_mCS}KW-%*)ZOpQq zwbD{p6Ik4=O4eH2ym6~zg)4d0jgDG|#v!wRpvUbzf1B+C&Sic|5WAKV%S$rLtT=r! zgJU4e2`F1+YOK7mow200WY)E8K&!+!{Ba>0*oMYNlPAY0tgvQQ##jlInmh`PkrYQU z%f_q%W7=XvTh5q+6q5*w#yH1}g={uwv&-Y*r&L)JE0tw%tR;Hug1 zS)Iq@L1XcXD%Nk|i~%gw7OOd9+G3ZpRM_T?yJ-rz<0@yBL8;T@)HtkhDQvWP!WoCk zQe$JFqMUIB#-^6sN?_yA{%amXD`%W4%ZfIDWfdDsT^`l6RnJmuwU8RiAG?y3<|eY? zjakrIVR79rdt(=~EB$aCUmrWL9l*KdkJx~vISy`xE%+s=UTn)MEUv^@)vSSaDa-Up zbu14AR&lY8GnsJ*EIhGnaSl|trZulFW!;?R$#<4xgqnD@$DY6J4>5u}pDrD^@A6oyHPf;Oa$r6|ulK zxHU#p*Sz>637r0mJg znCi+tPj|j-#80WU?C@|^Uly#QElX_ejm4{h<#94&3~EDXan%W}bXJegVT)G^)5+J} zPVijxV-nYV<1H@Ctq9n`D-}Mg&!-^3WWMS{=$6(;Ev}pbC$i z)T)vVp*6P|LN$%G!sd~n>P@^Rw5n#o>cDk^)r!8e2!`$D0{|7w3S9BZgESVHmOEZU zh2>i#mk|_K1;$azPOv4pHpbPM1*=)hhA$Sfnt&p%6j>g;2&@)(rOrnFglnHMtNEu0 zpd_*}|CH5iU(5nm$ZSFzI!kNJRV^M7D`=a{x+lx2MbWCfSV|ky6kt{_Op&Dt&4#o^-~^t@5Wlp{F5RnQ_xuYb=Qk zXrsrI%tl)_Y;i#=TfDfK?YcaT+NfkHw(RtfTT(mGSs*)E)#u@<0es^N${Yh!16kZ6 zuCZLo4qLoJbT0j6w?A0dLSDJT9|PBrS%J$ON=v?&MXhw!#NwukSeoNF`>vVe=*>=t zDPqnTz_OmDGX}14HV;LXZ(fl-X=L#oY{_PiCtsFA!%sB;#i2!Yd5TzyEvs3UvNpys zuxM&wiKCjOFXqymapgZ@B77Rr^OXOr9mdLCf4B-k2U)wk*!*m zh8QXfT zJc^&f9V>Ur@TnR-RM=Q-^oZIPKCvk~amJ`-oftEVBBL#V(tCky3ata z<|T%VW0-;kZTRALYJgw-DMuE;EcMlIG{sTQidc7NA+(fQ@(N<{HkqbCUhFQ9m6kf& zUj8Y_4Q4xQ9K6yP^Np5M`rP>CsaV2^6K_aUZYMDpZ`0z^XfQ{t#bqx0_ zQKbC-|3C^I2`LNR_SFIdTR1?-N*r&!;PbVF)dpW|h z+%KJQ6|BdsLYWH7d(3M2-qn=Plu#$ZtByMAsH1V=I{H;dy$V;a>eefH`;|Dpa;!h( z4t(^Zr=0RpEFI#=_y5l>{t|qaJy4u9iE3RP724Im5uz6>$od!N^Jj1e# z%QESfGq63tbC6g3UB0`CYkl_Of07^G#J^t2zt+=g{|)#v_3v-R?fmufnq%f|`58Tq z_%8W3yoGm9JMD}!PCxCm)6Y2lm%lpw^fOL7{j^_cop$;kVC6SCZ3S2RHS)XZBWKjd z^TThtAacjjzr;(?8gvPxpuMqc6SAJo7j872+!`{fS%hQGU^GE&I*8&$+NS z^ViF3-b?&`e*ZV#^7;X~&*C>c``imIxM1D6=U=eyjNh$4|AO@wtUvqrTPZkhj8PZ^Hy%@8}N5ozi!>S{N@`jIRAY7K=}6S*2T9z_ndRF{2BT; z_>nKTU?qRDt^1wv+xSO6_pGzdIrkj?$o<0a=Y?MbzsDxN*V%2IgVo;Ct#$nu=FVx= z2hZPt_q#`nHo1F0ZpNqJhnXz@9z5oqqicSEt9|bDo3@WHx}~262mFQpjc(~3egHqs z?BC=M=&j->=%38|MCo@2J0JG<>JNNAM7o2+*ZsIGiKacm?IkP`f|Kn}>ZSJf7Rb!Sve(dJ? z`LnLXb^F%sd#~T1#~Hspw#JW<^Umet+V=^6RNZka`jc(lZ_7KMKl!@z&c#p8XWq%G z`^X>N#P`}b((5N*dC@zum-QVtnX`;j88@{2{LM$w$A%wmxk3FQ9Kc^Lwx8PquZxY5ZRK6K>+y zjPIq#4IeZ0i1Is29Q_6T7red1r)=s&oA{Z=Uth=_AhUR?UVp*)c!J>9;Jt}E<3fJ9 zP5v|8&oiI3sr!Db{+{0}zQD}4oxaevk z$WPPt>-IPN1)B~?DC!GM-ph|Jy6(T;xVA8+mS?d^Onb&HZ=zJ;(J*?f{i_{dzn}@}V71 z!sVaa6YVy+<+>^uN{_BYXwj4OHu?a+S5$K>3uf2!yIkOGp#7>T(bG@+KlonmW&G&t z&cluPUDvNa@4WblS^h5h7mI(Rwg6hce?#p3Ti^99#U|J_-O|A9U-^RLFcE3pIIX?)#v=RjzA+d8}c+;eB1 zXSeCQ{6<{yMOR#B^N06$ojLXrb3uM)UT-%#JM-8$U-_bcQ}z+!mqZ=A^k4Lw;M+4_ zaKq;PyP1DA-d%|uV1EN&_q?+vf&O`CFKYO$#5eyx@HMm4`#)fV#tw`f7&|a_VC=xyfw2Q)2gVMJ9T+<>c3|wl*nzPFV+Y0# zj2##|Fm_<)fV#tw`f7&|a_VC=xyfw2Q)2gVMJ z9T+<>c3|wl*nzPFV+Y0#j2##|Fm_<)fV#tw`f z7&|a_VC=xyfw2Q)2gVMJ9T+<>c3|wl*nzPFV+Y0#j2##|Fm_<)fV#tw`f7&|a_VC=xyfw2Q)2gVMJ9T+<>c3|wl*nzPFV+Y0#j2##| zFm_<)fV#tw`f7&|a_VC=xyfw2Q)2gVMJ9T+<> zc3|wl*nzPFV+Y0#j2##|Fm_<)fV#tw`f7&|a_ zVC=xyfw2Q)2gVMJ9T+<>c3|wl*nzPFV+Y0#j2##|Fm_<)fV#tw`f7&|a_VC=xyfw2Q)2gVMJ9T+<>c3|wl*nzPFV+Y0#j2##|Fm_<< zz}SJY17ioq4vZZbJ1}-&?7-N8u>)fV#tw`f7&|a_VC=xyfw2Q)2gVMJ9T+<>c3|wl z*nzPFV+Y0#j2##|Fm_<)fV#tw`f7&|a_VC=xy zfw2Q)2gVMJ9T+<>c3|wl*nzPFV+Y0#j2##|Fm_<O8?7Z{EFXnaeop-*(F1zlw`yQ9R%wCth+~u!$<$bSmwX0v_ntybyYya_n z`(5X{*ZmW&>s@dE{jZO8gB$$m4R3g(8{Oz8e|FP9|MS1N`7Lhwmw$EZ+Z=Gf?QVaE zJKp&&cfIS~?tYJZ+~c12y4St$eV_Z>m+Qd$9yr#8*@4U5>wsI_^iQvI&8uAL@_S$U zQoHTC>#n=(vg;*x-(%0qUjE8gyXJMSf8(3||PtFF5@07ryu)-h1x4z@u|GehJ51(@Cr#}1nFMsWu z-}(NJe)h}LPXEpSo_)@_>(Afh<7RxqrgmWOJN(6suD9Pc_PzW*m$~%ryY2$Cu)x%% z_T2mOSGwvSUHAGo`HNc}aL2pe^S}o@_+fwln8!cyAD{B{XFluM&pGtaLl49Kb@+>p zIP%Dsz4BE@uYS$3uYc29aA&>u11FyJ(NBE(GoSnN*Z$+%-~GXlfA))C{_2e1{Pur; zk2~zVb?eukfBv}FHg%sY_xF3<=0^Kp`|A5%;j(+}vD>bf*!f}?+i9nrE_U&qciD|( z?R)iWU++)<>=w7a{aya%-hca`zkB$jAA8UfpZv6E;J!NKkmnutf)~8t@Rz*gh?lAm;4 z($%gNVBP&*fBV3PJp7T5IY?j~{G8|VzQWVs5l6iA6|Xw#nAf}(RJ{#PfgkuFsQT1r z{{4$z{rWe*^SvMPt~%}4#OkcGcvr1kziz{N-cuJ`Fz%}5?viCczQ>LCyXIA|xX)g2 z7rX4NyXqnrxhTlG#BP_`^RidG$~E`f|3)|a%iG@JZudO!fe+?=^#nc%9*pO}=RN=M z7rpo;N51S8uUvJ^tB-yCamT;yo$qr6X0h*_rOO zfp`+!FpygIuGr*X?7sgsuX4r9?RDu(!3SQPR9yu3)kSe%UGh?wzU&qDy~cjmyWyYT z@-}z8%RL}i4}a8S4|)Qh0}npr(8CTp{6+K@uQ+PeF|U2y8;(03&wuY<_|VB8`^3Ng z`{%#()&Km~e}St1{>A^`3Gg@n`#Y%>o&h&Nu=xBpsM_Qmv&1LtcHOI8`SN>T24Y3W zjyntY)kSuSCqY=jYyZig-t?BYzQbMaabK7VJ_kPOsZWPo!3mnGSFM5ve8Zb@SJ48V z{PBPJ*MIx`m%jduZ~xc#byuB+yXv>U``y{+&{@D*46K&84>tEhm%P@MFMnCoSHudw zLUKjH+8Khi*X1c#e|pnf-1>HTUp@YbuN7{JxItKM|{+ure>_v5bm*r}iV*MI-Q zS9n)_|3^Rlx$den@dPNbqG2CcExrS`;3K>2d%3;$gjm5?$gf{?C*D;T+nIW`x1R(5 z?B=(*J-B+{Lmu|X$Nj@U@+t7p!=C@5m*}o~4W#NVC!|!tReb5I|MAUlefI}H`l)Qi zuLTvJ|7ZbuPYJ6n`0>W4b~|v1E9|xBrFKKU#CQp{l@W1LT_tnFhTs-xe z2Osi0+*L=s3}uyE#R*I+KJ*bk|KYCs&Ub(KpO52Kl?e-ieE#j&N>UOqO^)sjT(5%@1*z_J0q{SB%`II zis3DCi(Pl$<1+goxcKAifvelx;jTyo|BiC?)Mq~Dki!mt@sTfo)v9BT{U;Z|3n!lZ z(NjP5na@$GzWv?r|M(|-{?lFc`?KKy@%+apK%^Dp8Sp~iPyB}$-xC&aXJ<>Wfz+yt z1FKywd8s}3+UJT_xyF9`-|)}>^0r8C@AsgGJ@T;!J?UxBe9rS;@FF|`u6oU}kSbPH zANcS`K86JF^Iz8U-w%EaSMjUUQ33xi(h7QYxr(qABdyqqd+TC*)Ln%;3QPADvD$O* z%OSV;FToRF-BoK5tjntV`Hxbiy9&0# zWfd|&AVsJ~3A`2e)CCt@>{3i@tyJLw(Y!+MYLClY_HtLg+8R-RqBI599+Uei#AZXFvCaFX68GE(_qFqYpw?L9K#R@d;2F;D)XG zImbW7eYKmzbr-<`3r~T&;=bBzpDQ3|{}bixlq)#)CqCsF&pGr3FMcU(1+oeh!0%s+ zstN(%=fC)s|M&)4AU~q3;9bSCid0Q(#lULo9&tNe5;0)QEA$*Ftnd_gCCC-xKv;{r z-TVFzdKjF=)1Gw*EZ{4SVp#jOcjB&sRGs?ie?wJ;SpB;wfSCYh4VP}{&4KrS=-)r)iBEava}ImakuN{`)yKZ^ z__w|5z3c&_s`~5~(O3M|x4(x17*-ty@Ju`b{tmXn2Cz!t;VZWC{@MvuFz+kgQwZ3> z6k9o4gcfvxSA`2iN94A*zx%!K|B%0b z>=U2*>_ZNJ$;*#A=5=p)3$hASRUboJ@pE7P`hTzi@*`ALzoY>~tp59R&N-V@Apy)M zzya0P-(R?|E+$_QT%i(%wV(^+Q{Z*3f1{h->aXv7PmIPq`k<#g^SRG|@yno9Z#@2l zcm4Cihdy%ZCm>Z{{u&w}KR~Latd`Gz$ZFA9w5u2);MU(o^%X8EXTc{yy|E>{^DdV} zKV+XPTm_*8UEm$=cAp14L73`n#+F{l&{2kQ&OWapTmS_WY zf5}=vuJ9Dd-1Y`Hxdmn|?sdNh|NY~h^z`RG|0S>Wr27KyxbM{#as-szyt0ite)`HgAaoRJQlX% zJ?}?W`w8^wzrwK!JpcXoFaPIPXPogHT6Ja>Y*rB}rnSnI#(lLFcTo3FFRFYA&whLY zqzR<6zzEajuYA>OUH1kzxjEV)_j$xHPrytUB#>x&wuBh8-o=tfxGWDZf@NU@V?S>pv8)F?7QuL>C0f;>>6l?5G&MH z4}aW~p7ER)y!5EmuYdD9-t)c_Kl1T^`3%O&kSekM1yy+dQ>+gA2dfUM;3+V$v;jIb znA*Dgr~lWyi>Oq9N_DUd`=$53+?C-hpjNlO{oU^UfWLe6K~FpQ1uuE!t6%r#x4(PM zi6@`>$xm|x8h6zXfAZ6x{}1mf_&?26oF}NVs2cZFztvlP+m?c3X7voX+okv1=ZaSb zRyP7xcfRM}KKM}wJ>}r%z4#TYUw_=&-}8Pp>p%N>b`>#JrlQKdDn=Fbe=dRD03J|n z_5IU-0o_-TE5pU3wCb#=#(mXq^;X}$2(qP!ZD}k}2g9)Mb48wY0JZw- zJKyX64}H`Vp7z|sk2v}@Z+hE%-v7Z<5Uaygpa6zc{Q^?;>tAE8R{38Tz=)MD80GI) z-#Ig1fN_QHuSjio-5v8w`-EEE_D+9u;Da7{&{Lj$*paV#?d#wAZkTn9K!4#Y3ILHR z{nDZO?{fl>V-?gYc=f^5%tmj6cW3V_#7mdC>=mwZjcZ@;MmPJb+uij(cm{mZGY@^? zD_;Hjx4iRxC!YNAPk;7HcmfQm!kJZPastp*6|K7RKNVHm;Ab0OV!8uhieSMA@+lCF zE6l`P4yo;RZ}4ZgyzQOtaleP)8Ss$9U$*L>-u#Y#UO4IFpZe?v#-dtPx~r^I z9@b2zhEh$pd7HfFCqS8Y1nf+0fz`gm>Mt1DKIjpTfAYbHzx3#1k2~SL3n!iW$$!Jy zI^X&Zdi8V_zdrNKEPrV#6x1rJ*d{;O_#(4AK&J3UwNm2><4Zc@;08CnB)<`TK^5`c#{m>V^@-=Tb{yl3x^iep#uYLX7-_=+xXEo8N zSEGvYpWL6E`gr==R`<>9*O62qxyU3?J;rPO$qjFMs{`(O-v>VY@lSou;V(Vr^~b+! z%}F0U6(<3I^E?0bgP;8D7pI+$)9Yy}BB|AxrF{AuMtydJx8eJG3WTrNiE6d$?ijYc z!d0$$o&Epp7Pr0QJ?{UoNB`rqo`2-g$G-WU@B7dxpZF|C0KfOcpZ*Ld>!ANZsAB#l zCIEF;4Xn1|y)*q%lYm|4@!)r5XFct^G13vwb7r*M*H@)M1Cw}x)IKcn>#`n+x z{)MK>+^NSo!0XrZDNsqpplZ6g+xR{D3j6g-Tmmmaf*E$4BYcCK-12s4g*@V*r#%;9 z^~SfpckRiiej2BiQmcOcOHic~>pfGEQe~|gRBhw?r~h)ir?4UkR6F=u^y_!L$Ne7q zm?u5^`7d3KyaI0hQ%Ebm@n1hi1bEtMXQ%-7P(_r#(yEbF^gFxlZ)1EDcNR|#Ay%lZ zu6oV=ZgA6E-43PIBcJe$LytK6*tfiE&4)hrDKtSauKtssa9ET5-*Yfj5kZCKDn?eZ z?eCk}ugFnb&Y2-#zvLd5*;_9abK{%e2Bp;_9{-f*zUURNdE?vPcj75<>tFxYw||Hc zXuQ7CnK7!quA&OszmWjWZtXUIUtGbkqjSF-YO5>aC79sXZ~r$3KIE}adiLQjKjw{Z ze>aNYPb04Q&JTXf5kP1aUjjkpZw*xpQ?bqOm?geojj;Qq(|xbL-~Knb#Q}G@_XGd_ zAD;2NmmKxF)y92tf@DzyJ z3d2iRx+==xo89`3e{=tbp$bO2{)V@|@53Ma^yj{cb4&Gl6&S1Fd8O#o!~ZF$9p&$~ zy+bzg)!^BAf-ojxFua6b$RE?M-|fB+c@#b1F}SNv{OBh?`$f8SoUVwM7NMg@aY@b-);FQO6x;p1DT{F|uGt3aw zT@Z0uBMXZntAc_G5+n%-h(tjNMo`e-3cKoF+rQ7}eyeBD-*wIS$Ie`JF5ah3ojP^G z`#$gUeZKeoz3(TS|M;KZ@Yc8g&ih!j!1%%;dyD*!WUdm{&%IF7i0b(o_y6+i zV#O{ou44>Z9WJeH=@~n5@zJMW{rWe)_R81(%zyjUcfRj~zyIkk{P9<`fHAe=AOHD(!z&7P zNkzpA0?2b5lr^ZXV~lxb8+RH8cMToCF#EEfc>RC+e;{VCBLbJL*|cMS@zJKPp_vP_Prc^9yx}ds`EI5yeg03G3iwTy?2uX_ zuSBMTl{%^UdyW=;;a>VdQWoy#!!E?z_T-nPWjkNjLM@B@*uc)%e>y%pJ|nG26R{hwa{rvLtq_i6!2*?!}1*tYw- z|MQ>zF?2=Bspo3W7xc9sOg2bh6XI=q^6*yu$If0Q4F37IybW5BUjGW40h+OO!Hxnj zwSO-t9M5@huyV81Wve$qtE%Rn;o}z{dBv-L=1u?oo$vb)v~n)YH(9jp=px?gIS-Y- z_yV`U55^ubOIEDkvitB6Mc|q1PyEE|-tgA9z5Dk*`l-)-`D=d$t;pE2y_};UsR4X} z;dsu&{~#;5+1nctcz~2e_sEHhkG%XfKl8?4{jK+Y=o6p*<3DBM`rj}V%uE>87NoR# z&Zd09U;M$C6S#8iR=ibBTmQtlYft|8>)!a5-+b5m|KQV~XXd&ZkX4JU*><|-3tpY{ z1kcd|Kg2S|g-aQ?I#5JxH9T_(TK()Vz3sQ(`{7Ui(HF^Be1qB0tOao-I7R9?+VRDB z>4#i&zhdo{J%?FqFfe)M`Yn>SZ+Y9hKJf8B{QRFf$BcQK{}W#QQ>p-83^<2=pJM&?*Z=C9-*!W2X=TdxxsH>*_QJQozhAI$`Pxmp4wcrn_K%Zb zf8|fV;Vr-MuHXB}r@ru|ul~hf@l9hf$<{SHFMM6nGdy<-{CmDPX~VXCg%ypR!_#I0 zOfiL96`ohK}s?`5RBa8gGRN{P<_S_+=(< z;;p{(U7}!ztx{q=PfNb=FaL)XYw%V@RV{tvC$BvAs-OC~Uw+$fzyA+D`MEE#+veZg z(d*1wO5N29kHzzy;s069j2MRQnY_f>;Gg*EH~h*w-}RwSNGm54l*HcQNId^3et5%{-G@jo z4Njc7cI(G~`VDVowbjS|@C%_VoMaIzP1D%w`5W>@c*l=y-oB5)5VE!xW=SuFviKz9 zR;=L;TCpfjW$_}^Bt6CRwZKcZ?Fzv@Zr1j7zwpa%fA7{R(UBX?Z!PScb#J}MrKbowK8$EIP<|}{d z7vB8qzxDo){^940t-i?$9%vQY*Zv!hlfLDJZ-JK{VDGv1!O61^Kk?&Ei)Iz$rk#stm}fATY5{OZ@)$|t5-u!|$}EK(adPvCm6vAK8jn2~@Wiv}Fjcu0Q!7U;m50@(%J#pJApMD)8_Bfd!{-5SSu0Uy!h{9!KBh0-dKJ@`-u}BE z_?XLs+02Li99bEH2~0<><`Gtl`m;%J#b^jiLq7Y*UvXuKE4lgFG1eQT(bfC`YjJku z#N|g{@smIQ=C{4;y&oZMOCZeRI0Sn#e`)^IG_A>!vGFrk9%tkVW${54GJe^m+yqyN z2~1fvpL)oW$*Hq9UiO-wdDC0p!8g000{K>hZ~gsu{s9x{H>A&}s(#5MUO$YD4@a3=m`Clh%L!4@s zPPCX$P~BdB-d!DE&jR81eB|Sw`O;Uv{#UW7E$f5X(l%9r^9!qsmmYh?Yk%g=zxLbg zll58VFa7nm{w~=h#yK(bs;b}jm05Z9>aAD3?hU{4_TT*=CeS5UfA?*IE8<`m2&Z7p zrw+36`oo$)wjTNLCs@pl2_zTzo$oRp?1=Dus%cu6RW~1f>NP*}-+t}4*eJ`b0{<$8 zL)^{+6PRK(uR6%;TQ~V4ieG;F@BH2${2`-^rr6zsJ+`$?ZQ#6u>h_u^Z}I&uDt4AK zCM$&f!X#7dskE41P~Beh^i!|q8?N5@zK?v0FE?O63%=f-9XS8_pG@1P(qdkrwf2>- zVCd>?zr(j1unqfP{PnlK&1!D89!aN0&nuXsGV6ch$6xo0%nM}b>W{zjHFisNOCP63 zJIa{yYF+`h{x$#U^>2LZJKyu+PpATug*>r9E*%k`S2$Uj^*@DT|JC1Sv%t^tr6G*o zv#(>i;$Ysv6cyU|`b4p_u_NE{_l>`a4K4g8vQ${iJHR&m>`(vVTYmF**)NqH=v;L5 zo$r2!8BB~^rILNVfwlSPUjL?Fd&hejx?(xESh?!T{g=|5#e9S6_U2#wSr*8VU}9Ud zzwn#m{>OL8+xlfZsR*2JU~PHh&;Qcf-sLoQzK!P_fBXNkqeXnjb1K;98&bD#Z+-I{ ze&rqS{op4)^Z7r8R$`UpZPOi7=OI>adDE})J%6A2+!y)gBvxIq|C0Lo1>hoV_*ZjXFX~85;ycIey^;h!^uI+Do%Wu5<10VmRFMj#YzVS_?D|qGT zeJTUz8&bD#?|8>sfAc+j(VrU;vso73^G9^$0=bk{^A4;XzvWIb%#HpU69d^cpFo(c zVsuw2So006UGM&l-~Hgn`I@V*vkfQG!nx6StCUys4Jhif`+e_t-$$5f@zt+?gL&-V zncLMzBRCH$`+Z)eJs*14hgf*}rLVBHL?>GI$WU~~ic?T2~+W&_iU}Li{eU+iB z_ySE<3ICsv>?yD29bEhW$gle2JA}W<@(^a5L94_D&bykX6*}0OZr{%POGF^F@{4KRD#qcE)CA5`tiJKZKmD`6j4!5T z8as2N!(GkKx_-acs^D))FR`l+aWErS$(E^UXf;23Md9DEkjF0(b|>Qa%KOv;&QGLn z-!A%xuknrdtmkG<%>PmZM$%%w*7p0mUd8|G&m9jY!#=mKStjf3YUP*keqHnA160?pdSM#QPKn6FUXF1`J? z-}YNx=Dby^Eaojzw{IW0{VgmYvDKZykknVrTeObcb~rfsHi{U#PY3VkF;>ir)&Lq> zsn=6!F>kRdyX|OjByA%OPH8o7p}Jju+f_W!%3TuD1qbsNC~78Fm_T>8jX5#%y0YKr zl`FsfKV2LLRDkt;-WK!y^2={KY8A_PQdKd}!K%2;&Lhk-bN13auj==G^~CBPW&PjEoW_rCHk{^3Ksn!Jg>@&5R?w6v&* zTa?n$(sIkuJEf+kroO(usi`%SY0Y$Hy1IIMxeW{q3=Ito9h)mtlv8KVo|>Udahu{c z#cQLZqeHxPV4%0Ft1HuLX=>td^&ItRoX2w~#j0@jcw(*=M|sbkdib}--+x&$9jz^G zZPcx`Khv9O$Y!%OHQ7x6aAu&tYoNcce_&v6ptrA&KiU4lfqs5?uD`dZSERZ++B-Vh z+uGV%Tbdi{8|tbnD*@}skt3xg#f1g=c{F7~A;k(77Z;b56ak#Ym-6xs=jR>H%RhV= zR1Y0K%6i+gn>&ni}fs>uPIj zswx4itgJM8YY`QvJxWSAzqKeSJyKR)QCU&J9fg`yaf{#TDyl@K{H?8p6q!tCSEj$a zxwk)C1GOk{3V!wXbq!||xL{NNKz2~z;MKrje{VMc(to%fz-n!4ZEb0&udU@Q6+l&X zq_n8GsL-YpETN)xxZ|-coV-G#0u^_nC0sl~>qiUwrsS>SAt(V0Z658;AqxvQUOg1!wLvid1VEyR8>`7NAor|w=}n9Xzs3_o^G$y zk{uZxJ2rl7;uv5BO7QB$sZ%FsPMkaeWPuiN4Gj&_m!ls`tj5Ory1KgRqZL3=R;q0& zrt)*W$VTEyibacZgsKE+;1<=X5VvY7MFFrL!RpC$xAZhNWHaNL%@PDtF(fRO-(JWEv?v_&aSTR?(QB+-(YqaYK@J9)#TLl zG!OyPDZn~)`m~T8pPqtTTEgtmV1IvKPe-P`xw*Niu>opT(G4nP78ZxQjapSyR8{fIr>o#qXIooWrn|eju|JzB&CK3qnJvv^`+GCpFldfg z1F$=5pGk;iRr=_2UF{v+qJ_z5X{6LvS64yf^0JcR(vqTrg1oR>ci@UlXR_I4CwQ{PZ;D`N}WGaVQ-=+)atPaPf|8ygQ|O&y;(0iRBs6sG{|#LNjz zAy#s1bOgsm3w3v9+S;IK7{Z!rY!1GhbHnktc&qDG=NC{hTCB9hx&TnQf1_v?6Vq5# zQ{C9si5KneL0#00&p~w;%#vaQ*%Xfu#wn zAQo6= z#Vygv{bQrq^KE(1325kC`mBUo$@6?lJn3`sd0SdD$BW)PsP{p@Xa4m$XeCynlbka- zV^AzuCRzX_R#41-p8Vn7mVseah>Y60TF`24#c5?aw1goshDS!mj!{BjOv9~{Cr_R_ zbJlX^+!-w4$&=I5lgGx!#zvxN_tBGqi>5&&*9sT?7z9vWffb!IdNWmpZW!res6{=F z$bLCy2crJ~aYbcSLtCb+CDV+vQnAm0H9Kbnk%E2w-P${S)c~aGAB1XHz}|jbS$B62 zn!U5Dvr~`NB3KQ$LIg%7UVt_Uqcn$AwA|dCjTy}hpQ6&*0(dpYA^{1A1S1-J4rsBF zRS66pEZ%{0)(hn8ko-XM5nkpNA_AO6$p2{so53>ocg`fzOm})Sf>5yP;8t@>YikAp z)!Eq-b}&1r9fVxybeIKL(CX~jbKK6IJA3BzsgozBfl@m-tlJpu*7)Ef5<^&v<2qVd zfykgP+JvzE7zQsYd~;zCT_*}k7-d8TSOsE7Go6hs1H@O-3S9xGEVHHKIIN!jzJbo} zZX^Z<06hU#a0{sV`@s-~cVm(Xu{4E$x`u9x9PqxV=1+Q?#S*>vU|5qJue&Yn9@3AB!*6DJWF zBg3F&c%uZPZ5o>zYGErX0a0jFqd(`G1uU)|8-wAnq9J$$OG0Uw%R<#>TN}H3GDgA) zR7CP98xguN_he zD26!#D*eD*0WIL+70;&fNE`e{fmo|WL#QC^BHZdzZVe5Oj1g-QXW_F>&JY-% zId}ft`3o1gKYLoQH9dtL938 zj}oj1%xFuoDzB)ik4!}k!XotgU9&=^vLG~0Y+z3>U=5P8z+*wIVNn|#=;xVU#b7Tq zN4s}om#I*56JeJ6g1%5%j8UdGw5|=D+^k?ikT7(>q*Jno1+2EcK&Wk#Vnh3RUI?u4 zMbYZAfx!P6=J4*o1Z;SC=X`vIf59$r017T!=7i6t4d(n+cr892UP8I7A|%+Vqjkj# zZe=>!JKT|}+d(%!g*;9dwIGCYC$Z1GMV-^h}A?mLF`A*;egm%^cXaf0_4a825P>jiNU#8H05fPDfq4M0F0g7QoFG!KsU|9r$fY2~ z*=aNJvRNzw5QXN~3TOy~BA}5dypbZ5VJijIha?umM2giMjB#0W$4rjwGpNA0VBe^Z z0-EFwBS#ZJrm%dnFRGMOi&OG;-Vz-exJ(w*5gIo(HgR2$D}yFfV~AGdoTerxr--zS ziBFwA3&GA~3eWT4^hwhh$8lWaqr;e?!K}HAt_~o_7LqlH%oA=MQ)q7vtKyw~H~KQy zk*FQ*0NItYvT^bi8BEF34*K=5D<74)`V~IB(DLOff8gfix$U= zmKIeZiNhy<1c2v6iWfC}i84=YCost$imxY+LC8-;kDAlXm>v}^(S0p=H4v0ELoMPh z1bh#TFg&Klz|^VT(SxUFLh&LYO|{cI+U=uCKp8iL{1?heo{7 zUxxVQB}_vP?w3HlB?JcUlSo2+f!qlSB(0dV5v}4)tfB-+ASP78=S#jC|Hq5*wrEK) zFA7!#)HZMeO?<@8s0$Jmuz*G8ySM`qDaC(2n&Xv~Ah-}NBUGWX@Bs6pE#w+1h(eHW zQ>XDEZ=KWRmgNLdZw%X;@pRcqZdL z7atpO;n)F!cJexPToGr6OW^vsG%6OsL}^K;@foN9iV|2!D+$&MU|sk{kU&VttAU}x zp&@na$Pj1eA>6_oQ+MA$s6{Bz+-#U$Ew!L{Bpd<*J2b_Xj2@h{OachJl3lz{D3Uh{ znB^PM=h27cjUsqjSS*CKwY6Svx zkLcPo5bc7+3oj%a6~HH7Cl)l7=J<#t28*Q?Zroulg4pT85QJVRD?*vW`!Iwqn)U;tDAu>MF!+T zy%TAjkXhJ4pt^YJ@|8tLQD)VAT&@Jz+QobE*&L;&FTxGkb+I+tMXXprn6O6r^Djl@Y@}#gRXGUL@;1MwAZf@1CR2%%Op zDuaw}MpL6592~|9k|7MIWzzlBnezeGl`EI9gW_e398s4+SEAv=Vc^_}km=QRj4MVQ zRmM4=$!16}M7)rp4-sHR+B^UfD2h;pvS`o*!l=&PPAn_}22uePQUX#X1@_ZXjQDV0 zjIog)^sTzAIU`8OXLFRObGZ1RNwmBVpRsJhMF~^q1GEEg5`|~&ny?5oe-j-Z#*qI6 ziIV(DTyU7RpqlTOVsXyz0SVOuI^NNj;N8In&k#~D732-%+9(?djR zQ;{RCL&7`WNp+&N!V&_KBd`v`9z1wR%x1G%Kyiu!SV#*^U^6x?)6?0HyMkdvQjiML z69z&l7=;9yF!ROPNvAMc!t1y!V)S<81&QTqQtlWKq`_U%*4-dz1^)}IG=Vh@bBeu> z#4V!T#AQE)D0nC=G_>cG4rnu86KIW?)G$7WSY`&{_MFPmwqVi0#4KKqzEUbkB1e!aPjihtAd5Z z;C2~;UA%CPnCrxe=_zv>W20HpPknGpG1k(^h?zt64E|MAaAl&U1w3?c|GxbP4;|dM zZy$KwHO`Rz>>R83qYccEw6Jvb_jCglK`^nESdm+TSCP7$m}Go(njeO9oN6#KLdeBi zdQkdg-J2TKg7lThUmzs{E1H@{pdsckvrQ<8beAqv@(!RhyZdS4vG^G!4&he?z| zyzW0XHcxSMy^w7j?XLFoU+)Bj+!3Q5dLVv}`j-+4f+P-~3{MxwhLgC~s>Hw5FaA4n_-Fx@$1FOulNC_2MBilfzs|oC*?Fafv z2Vw&Ioz8<|*8tjw%&(AYCfb`M+8ZB5!y_?pS}}OtU}6wEJU14q%L85D6Ld31p+ugf3n*f8~*q`#Xj(xK)PPl?F;H8a;p#EJuGMw?>mY9_n3S z=AQ($AeThq?RKc7e|UNVF2GV0!Yu}l3AfN0Br?<)nJ#BJ1h>YN87I&e7cN}Bbm=mr zx_0gQwTB;m`1v@z@-eV$>93%QONY}9_CztQ$JE~Sn)@h3;#}>oSDV~b6Uy-S;`TEW5)b@ z%qru70gH)EoEEl(jq{@E`4%ji08!8w=n8<;0LCx!B1C$Ck~CWo!;kUlYOvH={~5)l`JjhTF3?y4IfEOCbW`}T8 z^qgU(-?545$>V4qrWl+e>*d_5X_Ax#dqW`zL%6px0i(X!4$D~s1G{PX7#lsX@)?2h zAMWGEzknx-zE@F4(`mr`RDk6_{D)PNIRTV}K}JCwx(3%7W>BOO;N)BUBe!VpWMYt` z0f82I)T)G8eAHrC=(#}59zmfVoRbqIXwjMumQj@Sp-@d!pm2# z3DxW)kKB0pVT_?V9(tKBBuFOa8XFtR;yZ9%&b36wJA{mik6{yh2MPFmbl`rl+67ks znpV0iOdwF9!$E40gzexEj%viX4+}Uk%^XI@tDSOnieAEm9-LO92(yDkdZcklqck^= z{I{>_EEB3A48m?j187^68N5ir3Q~L^<(|PcX>eOg&}=f@4*w=nAwUo$u!1tO#*tLs z#CsFn$J+%h{#9AT3@#emsdw-T==kMcYVk{jE1ft;2^Ldd`-!LLw45fv+8KK&0>(>> zxEK@aQDkIE6OJF7LSdXfeeS{qW8whokw<1{Z(O@}{b~pev%AhPL}!Q=mT+j$+z+~p zF4y>Nx73MHr*F5d65G%GG}-VOL&YZnPe_0FQP2R)*|7K0|+xH3|zUFEnDD?|3$|QaO4tiNs4_r zprP?>eVq`^?I+_7j}=YGe?Vd@TR2lwvVyXUURFQNaqg#|=e zkjQ{mU8M2FDoZes#r~P6z#JniCc!FZ#YLu)WF=F0n0rb}nQ0+dVDc9c=g~j78oCDw zcL+mD0x-OOuAPP!s%Qi5aR^u&-lq8r2LY0_4-FnzaVIeXucYNPfwrBLkxjbBIawEQ z@d=CQ1?>_PZRMlpj1qLr^a&Crj9}18V$Pk*qbFwx%&dEIPU+=ms!8h-A!`cT7~O(f za}#u#?|F&=<8v}Ad={qg#*Ig2uU~)o8WQmean}XfEv6NQkRHaz&~m7R&dB|kEQDOT z`ywQv!dW0v6ElR)U2UjHcHP#z+VU^hC<| z#G-PZfXgmom*Kd)$a{5Ms6x`ZPI&6YNZ~hKLuwl9L`AA9m0h9m?b;hO+AQk~FO$7KDlRPTG$)pd~0a zct8PEyoHi@vl0~<5G8LCtE8R6kBFQf^^>$2#l{8^?VI8u;f`bzMZ^P{2i_2jiZ>B? z3KpG7JD4y`;CPWcK8W^@ZjxwADoX`0;1SAs&H#%8;O)Ngtr_WCyGjDoK6FZnpAUq?%a7oym)XHUwMLY&|~9R zL1hghl*uxHMTn)nD=x&g$)re!a-}earWPoEX$Bpa?h7ZDs4%heWm+sy@m+%I?GDPk zB2b_eP=!4cgTTt+pLwWiv z;hveU%luwA&pA)ZEKcY+%|L$-L{0{2(4U?Q(nuH5-Eo0jqu0+Z3Re$U$_~mLR&(Zt z*8>e3$W{t~=RW3Vbx9nE7*$ryc_Jbp+8%Eigh>=1BWltsh?8KY2~6I`+XJmT9}sGF zCC<*1h7I7a=La%!PyV&?5;x*4$BrADu!I;wCeWzIMoAgq+HEW3y9(oBuzK_{h$XeI zlg?1X3l~o0I7UgGG33l6A}+_c5Sb=+sYH2cVc|1a-4(PNpUI3*WyiAvBO}L1_?bX* zaS>s56sIpb1j^~jqrhQg&c(|Nf?k4MXA$s#g~be)g_Bgd6D|38l#as&6D9h%jIsIa>5r&teG)$^^gDJD z$^jtJEZ~qbRUB|<@jf9~;N~<(gmY9NPA`M04^rBCL3Qt`OymjiThNQ-lbNTku9%w^ zp{NeSF|CjoHv%i}9)V>9T$sWuW_HnMG#zLi8y_FWZ;^(@<>N(?jDjaEeip0nR+*V> z|5WeDxZVm@fz{N6X^G>kVwstm##f=%V@&Nlxqg7M5HbMT#fx;0GxQG3;3(3u4=2gl z9Cx&10E5wrSlO@%Ry1VdCun@$V^hc;nn8L2aM(AHNGzO?2rfUCD)3IwOR(}e`5;*^ zOCZ5u)>5M z0=tkI_$}vD4-$_~Fo}=`JA)A-H+T&x_{d`vzyhut$oPi|x$s-|bW=}IYm_l}sl}{j z6k#h#crUa3NYOLA0;}2a%na$J?7-;g#0adKWV-Lv6og`~4kFJ4psZ4^>o9{=4%65@ zB-e$IHH^|=cO9Z3SWpX+@Q+yqMpDFAwtqBrczBHz#)+2amj<8(#5H`9rc(fYH@p!g zf$~1yCmD4fwiwUru{3aiL695t5+U$OKII3)pJ5AWS8xIwESEr;-fMEY98UtXexDp3L3kMmLoA>|aLt+azN(obyM-OH%)=$I1;8RBOI@&bDB>UDkAV~+yY zV>f|pmaq$IU8VUV50Aswhh&GCO52MfWPnM-RAoM<8zH!#IymGP9!wIb9q%EAm15_f6@&2SMY&@&(w zQk=`91?}H=iY-b30_zl{l2_78gBb+!bsh>82e6$0DT3Aj%TDiWU=>FV_5^7|U+^-Y zu4Fhqj3uvPCCR|jw2mKwlV~;i9AK%H`K%C^TOrS7cd!CcQLDnyl5V7xOe$8*G|kY2 zt?iEUN7e}gH99`QN=OnJxGiF>S&-s(>({I1WueKp>rxzK?t01sw*RIJ&W3Nu33z@^%z*+<({$;M1-!rwoLrxI z=VNe-b9k=6L!5Mf+yXce3zYC#R7aIjWgqZ}tr`6mOYDY5hK5Ekg}V7Om?H%t^5W53 zk3V+v)}vtu6Ra*U($9=th{ZG(rl=tY@%hY;btup!h;vwJ0r?@e8XsXOjDqzey+4KY zcQu4qox~l{InE&~E|7t|3~?`$;!@NQNXO_Mb_0&?v8rqQ$MaKQ9&lQ$1J8SW zav818)D^94mVskuWRO8PN4w#+zzR}5_82~k1Uz{Rd=`_`9Op+10xnqf&S8~|1ky6! zgB7&OPR+o z6b90rsP0afK$By-z`!o9!@%0mV<;LdFFJx3eN~W3UWG@X!GtD4ACLkQQ3($bc=-fv zp#sd1G8 zkO1#0r{tun8n~sCM1f*<1#66fV#m?ul)INLaWL{+4sBs;+4(Uz{DPHGC51~)* zca=Z9!8h4x@hHKL)=SV6D$Sxc4k%=Y7S=WotbELa;FZEcw4AI?(QUDKEWlD6XbC}- zddZQ5UYt4j#PJNpgpCSN8IUqIG>SJfO$Z5OQQ6uChK{j>tYC#(!{*YIt8XL>{3C;6sid@oEKjG?u0bvCw*ZokKP@<)s4A8jE= zRmwFq;&}v0lz3~juJ7{G{1YXxstw8B;(Vge7$TlF=+@%{VacvMr2~ z8F1`QrT%L6t^}&g>2t%A<0y6g)QM?a6tqJ7nM?=z^Q4Sf5XRIu>@5Xbdt>(cjmHS? zbdrJv=#Hk*Q6LspkeHg42@YnN#-#Xr{_zsQgcw~?EzMTvnawK_~)cQz(QdJT8cwF7YE=L zP)==n;2CkHl_>lqE8OdaL6Qij7f{pjS$rAua$W3Q~n% zRK;})S>x;13FYL~qmQa-_!D`PtAIuF*dfmmHU>gov@<=+>GpI=8kCVHJXCVYbKO%z zCsLRaE;aDtAGt;74ChpLw3!x2v!=iCSV0++uL*0Un1l^)6np@vz*eBb$t581UkDXG zpJ0U$1S`RcJDNvY;>f@pcmgj@n$XHl&e5QhrL>qBW6m0oVRd-hZf1rC!(9zpCNfZj z#9Qna#5mya@E9>GuImI`;@657R zrL(B z0-Qh`g|3DjB>IlANGDdecbe7h*A$M7p)`((*d!V8W5;mq3_%SHxH17lgx7FwLQAMr zPVQ(vX8|iLpks{L)5E7mFo092^qJ#WKy@EZk0C{rpS02!k_>U^3{Q34QRzsSfD>3f z$_+Ib^K~vlEOxzt(j-f;yiCBr$C}|sq}ysoxq2?22lh{BrK>Ux&oB4!ho= ztc-{ftP)$KLA2o`GL6tme%Y!N2Bz>Zyg}-y2Y7*FU?Od98eQx7FR_AI_GG54hbROk zKCcku3?4QPYzK>ep%GO6K%lVRm;-E}>ylhR5F@Lq3~Yk+rY|sd%(`PIRu3@e3BX3j zqKpi~HGpy)2{;2OoCf`coOPjAP0D``9Ao}WaVF#0F27;^Wvm#;%gbkp zJ#+6%)EJzKdShq_kI59e4NZR^!-_0jaIxVatJ-wytPBMs_X?5Uir88&Eit#OnvqwO z@tM0UQ*81O#ESNu>#JdknZXQ=lzz{a=(BW8<+^Q4!T5y(2^UYBiwsavY|fxg z!Yg4&XalI>zxdVU9n6G7Z#F_%qgi;El;Q#YG4oH4EO-7 zAC1B))xZi9$Y99KVnP+xo#5B(CS*u9{{V+7O)B{$UonU z)i`-3XoU?lxvvonJ0X;#li34-3ELWmMw#V? zNq3pDAQBd)X~sg1sn=2aNXpaBqF|&RVv(&i*N3biUGWUBuy?bMJchi$2Ht!ODQK7# zlL@Y#C$gIwGtK0jQv8+lHrb@hqQ`r+wugm!nS$klOtIUM;?4(=ViE`5tKW#0MhBAy z0t5~reG;3;tMQiP$h?AeqiuL2X9NHWfmetH?IL{Q)LO={iM&@}<86Gz>4@lNmu|Ge|y>*rh`tTeP>XUZDRbYR_$D zI4ON1(r}no2}=jy;{Xp~*&__BFo7o6Nm*E?NYM9ldN zQb#=WFxVz6NeF7&*#GT`-Wdk?H{QqtUPj2#Hjzq-cqwsJzQqCpW|5OmOLFn21o6|az=sNj{dg66bMqsXj+{Db=s95{IJFeIbFnVHW34;MrI={i=& z$QvJqh1RR{^M%mgwPGCE3pb$puX@!U?GX7mHa z4p@<{FbbAbmmM49Cz<Z!tb6Rdm zXiR_6zR61tXXT<-NGq*il)zV4BdG;S*Li@hGyJ3|Ukiifp#2^DF zn5vEegjOLdKq-)r*cuGf!i5fZesQ zu@pjZd1X7>A~Z8iLTlJlV`x=9IENT~D#I)Wuk4Gyg-c8%2Hg_G>6GFgMvJ>>WP(N; z0>l!~d>~DrA+HCWIfxml5O_d^%eTZSK*bMYI{AC&#*@ige3u#q6-Xtu0f<#&cjSY# z!d!`wNa3egiWp=FjiCzzLKgYOw0JeEbk#&8P8TWbj1&$P(*XBU_Uzux1oj;}ckSB6 z!##WU@Vjdct4w&S(U@65b}@2GB$rg{PX9X>;~(*`Cwrtw8Y4fJdTefW5zIE&N9iX;zhggxb=pQD&l2(%pE}o>2voO43z7kWS z#LDRcml%T%tl-t$AfuXHZJ`c?SL(nU4_~`vEIc(n%Kp8{kV5|{1W|*@{6M@G(<%cilK{YjbG8@>CG6s8v@yW*F41z>1&C?$_!r|zzev=l zyh<{?Dwk+6>VOZr!!nuar#Kb|@i3Y^3jf%1qC-)|z$(9hohI4N5<(eSfs`PLRQw+D z37?Z|u?eN#4W;2EA+FSZiHAa%3iLLE z6)6j7MQk<6K-Ux*CKNkholXiiFxbVQ3ngTQNO411r~yN>hY*BZi47F1nd!-~F|dMI z$Q?sG76G={$l9$?1@>PX!*PX(tc1rEzh(mIgA@~Myo3{f7Auo?2q{Y9{^D>U18m%& zjIC?)>X&TxfC?qyNSPd>-J_30ph@dvgLpe1_2a(J$6@q1P6e42iSI-idX1h+HBIeM zX%Z$p6b2ADL?sB70H)jWIU_Dz7kFU~@m{2Oji61^U_K8YJbdWDzFj-FZ{56M{ra`5 zR<2mNY6W*ISFK*NcFme~8#Zm;ym`~6yWD~^JKj4vK0Y+=+(V#ZS}j)a%vn4Z<5i~Z zp;eLz1SE6?NJ*@iNU8xuT!gz~3=3W{QHC57>A>+Z6HLsHQLY=Z$94)N1u%ljGMgM1p^S*Oj|79G?pi=# zG6NjR5~0xe09ztwsDzk-k4ys)4ng9pm^I8)DH%j8c5jy=O`cC7?F!h>SJou^mM4Tl zvQc8~e1ee@?!ci#Ec{g`9N2#lM4_8HyueBR;CW!*eoWt%%^NqYUA1b}iieggUc7kG z;>AmrEM2~0#qt%aq1T2D8#l2AmHC_fG0$vFUd=Gdh_Ij#Rbc=HN?mc~QcQ&js2pTW zu)+WaPQXPhtOayf`Z9Y2oN)Qv&_GY@#)b_f`fGC@sjeQUbH`WFIP9vx#>#+WF3k}I z_lWeNy9`S)>;FCs#DA~1vs(-!UPV90|z#28N3p!Pz~m+1IWbl zz*ag-*xb^DSsKuzFsbYaGy-MPkJqR#y z+qd^XI4k}^)ZNi_$c7Mhpee?LO?im%y?gfT*tTWEx;3j-EMK~0(SijJ-hclC_dl?3 z;o>EW!E5E3wV<_r18e1gYPe^d!h)R%M#2~nHWD@zmQ*GKNm#H%Pn_^qW-a)+&a}kY z8zuwc)Z>qH2Ud?T|Lpozrca$ZF*(LiOftPgV#zJ|fVa})B*|1-gB#$Hir z$c?&Sw_wT4e<5TlSVIGu%j;UZ*t%j$+hQxwU)>^*uRCDuqOZHPw!Glb!9Bb8iw&$f zxGx~V&beZA|Nesqz{=MolXrXfARCZ$=sE8|%-~Dg$NEzB#O94_Vbr3<3-7=0o_p@O z_uhN%yYGPq7cN?~_@U)1SFZsou!>n0;{&4uqm$#uj=5`>d5dHgAW0^9rLc(f(pBay z8C%IKVqu)sjajp{kCJD<`Pk#3ET9#0&oH8sqpo}CWelHvy~wLz1>qIOi?MP?9Vzgk zx+*MS><~Z)rL!7S@)r#p*1e!Gnlbzd{HR0qp>-potdlTdN+kzbPUG>WfD%~{`n6)&x1@x66=2Zs2&mQY; zE8XqvApcWsk0ZaKl=E~p1j43%j=iU zpK(BLcrZIQdE)GatB>BgIeY!W@u8N|{kyhr*}84}j-66wuS^lB{rDi4WbYBg-8)%9 zx^u_Q9ox49l%j6O9ALW;6x+Aqsn%lvmoHho;J&->x%)?c_$4pB`(Da}4=z}^NF%sf zW^K4DQWhiZcrZ3Pa%@~);jd1BRv_h&2v9lJXeJPxG=YH?(gK@j7))eE5i4XMykf2S z=@VeZ2tMN>jD`S>-z3ng90V-<4AGJCI*rFRy@Y}CiU38goEKk(AgTDD^fj3=TQr>i z(a2jUF~a_Z4MbrBmyRm@l~5)6K*T5^>NKH3rI#{{#X|uP@*okMs)fS5g0hw&H>bEf z`@}0={rX>g!yDiHmS1|~FTDOI|KqD({uDDWZ(P4}Zf24=q?z?}_Sg>Hxl82UwmAr}#62^xI@KwX3D8Z>ISD1nohFX_( z_B>cIc`0~>^}FJh4Z$lNmQeYV6y|4#w77Ww)bZnt3NzaxwiG06jlniqm4FpdmPW7& zZvj?BC#oieEu``{G%$k-v?c~Ba)XF7lDJI3;y!|6gX(|^&=qi6Hw9qGI44X+Kosi- zmuwm%Lxl31OU-7y({pS1L|GV$_?{E02SKYdLYK;98 z+gsba2F8ywYxd$r7Mz_L&-Qk{j5H$u)a%&5 z$8X(&UcswtmoA3Sb0SOh`CS*<|rP3h2qDO7ZN^^#4|Kdh6k_YRuSv? zn#a#yy!zySe(QTa`my)?@~dZ$_tYObyl4ByHOtY@3ou5Dmao~g|42joz=ZSr&YhST zY-?!j965FMk;fl@{N~k@1FbC`9j$fcc^W+Y5Cze<2b{2egt!Jsl#LrU5<+bNFHi$J z%9hQl^>u5OFI~EL!2@6guU?8Ny!+ml-gEzhixXm@1UImE7UO};3mqLB9Y$Or1X*KX zj)|lNy2o)wn8oCEB4IWFzzT-D;@&~wfC_HiVkW&^odoY_L#S$_V1_t?O2o=c%#gmU-9BHTf z)q&Q&ojdjfP$czs;K{ZD2}lXj`VAWZi`W*N@N(O>1z7qkq{XtOixw_;;NE*)DpW5O zuKVu4|3Mwrsx@ef4YRD|9_e=vz_D?bb~!hCT4Ke#7%X5S+LNROSOujpfyTmWi$^3D zHt-gGiyN^pL-@=PJPB4DlkF2aT4SCCayt|tgy<&HA&e;}{%5n-_(~81dgw%S z1=H;BS}K~=(Yj>34vK(}fhvqt)Hy!RGhyt=Ch$Jr3lY6X*TqADW~|*IA$M4UPl5%R z3i1yfqV{iG zBgaO^#sF)Y@vGz5K&24OVv`d^yVIBCRS*k*mCTI+E2wod!3tCGxMLy^D{LUyKt>w7 zGR^LrY^IjkSIR-17S9mDnlm76X$n+Z#|>n{Fges3j!LIOHbyBRLGU?RJN=Q#YJC)- zg}I9?V&aDQkP#C{2up`F;s5v--2LkbiQw|`3J&hsSKKyn<>~+W!H@p(Y)5fkY4hOp zsWTXHTocol&$AJj#iax7wS_x3Zri?jbJ)2p!7GR*RuBuY5<3X5fNJB$_2eubLRvg{ z|2-(cd+)#Z?z`{4=U#N+!iPvStzPGcGA_Cw8txxKT&M%bCnu(-lTKPJ0{OJ zXavCuii8wIQYZ~o?3?sgVD-QQW-acpw*oAPrM6I0Jn^zy&jp->; zQW3b37;J9u>=|TpERt|ojp1x>OLb|{k@{x(`_x3Hy0UKIEFJjzxvAlSf&PI3*3);E z?^(A7Z-m7|QG^XdB1n>Cx(qfu5GClDv|Jj!f6c=wMSx zab53;D-T~je{!^sU8ebtoYB#N>fNi?Yz$VdGgE{KTptEawA6xNC9yWhE3>xukz~62 zUKG3GaKyrd!fV%~@wb#_hPr@jm?^qhx0Z}Okp*ienRmvVTaBPlffxSDU^sjhGh;$o zJpRPXo_OM^r(X8t%btAl$tRwGWjC%c-(n_)8ckc^ow$6EBHvSwc5gD=qCNPZ3anJ#{>gJliF3^AJ~q(iaE){#q04ZHd45>RmvVZl680e}I&>(nsI02K zrK5j*`uGI-&YrfK;{1yCp4Qf`-nOHK#kJY9ESo+)>Q}k3r+x3>SWn4j0$CE}*gtQE zS3xZJ6hbhpAUUQL%O6^}5Qk;T;=X(19$GCn#k6+aM(pKy_fYR}-|*0Ac64OeBoj8- z-Nqvz)?+c_He?_hS>UvEy-AkodZH}|f}a4RmpwIC!0p!TH3mg6f~>|DD~up}tTE_^ z%vNEznvmeiB}K`QgglCt$KYx7)Pe#ON%#0yhoiQqNl=j27aF{_-X2ioXHjVwxI4*d~FYV-N3J8Hlg zlJ4W8^dmuH(ub7|ZQWV6{TLbO>TIbh$SbZaJ6hk;)=-jH+IH;h*^^Tv8dAO-tfOaS zxb5(|)uGp)!76cB(khe(M$jD7^5shwqS|#@_v5$jz27|hlI19Nu;RUZ-!orE>zFe=RGqhsy`d4)$RYxt_m!HKD{zK-_h(rv5O?kX&=Yi_J4 zD6Hu|cJlaGc7VCYEDq_+^bGY^?^?fh&APQ~R>LdhH%bBFo@AL+9(Xf`gXadX;MRk{ zbsw?SLWe|1vy%)2s|;G5b-hDb#315gjQvO0TJHFaF_wuY_GJ(%>|b(|S9mK{Uoklb ztO%-}coJqk^)jgiTu(i5^O5T~EXBo;sasd#%PVK=Fm6#ppruD(126SD|HTzPmK7OP4KMk+`d^TQgnIicNX=B0{h_ zMyd&2>aKHQWuhsd!db<%+e=Kia7E8GW}H1tqMi8_rUFrbx1OM20FfAsATaNig$xAy z-ORT|OC;I?o9CrhlWTBvRsF5=q39F}3LF=K5wIh|9~vxS6=u*G3c<1%FqU))Ofi^; zfurmpud`=Q@CXX+0;NdAk;gVe8i{S%g~(>@rm7QXyX(8A8JoIreyYE@uBo-5g6vb? zPWt%nyaJJeQ&f+x#Y$3jP@P(~wz{HN3KK+w7OX3;YV2T;WFp&HcX-|6B^wVGRn#0U zI#g8CJv!DeP{^nVQ@VSrw&`}(uGz3@10)I|hz$%YX!dp!Sgl&UVi{;H#A_{F_}~Kz z^j6DOXad*6L3qXX&&1j+GSOc}hy_+oy8tOuEbxl?(HK8|v4JiUVY@=th+JpA9Pt)D8XpgUDoOR2z$soj+&`}gcQP{V7AksUnj^2P*Y1+5 zTqHG$Ogm!B~MTOM{-ArW)caLj3XTVCEZVea@tSQ1Oa|wb z)_3)Hx70Us`Ih?H#!Oq`##O5~Y}Qd3Ek*SZ7cr_uZWr6QcJ-PSOP4;hc;RBGg#pA_ z0o5w73Uu8l&}HB3m5LF>1e(4Labd1qe??-9kw&Z_lg>i5qb?xWB^V~(BJ)lv!$QpE z&KhiSaS}m0G`2$WEWU`dzqDV@F4S9 zb{#;&mz3AGbdO9N8^|2pyJErOwLA9H>-OhYwe@tjy5~MPGm2eZ+t^yQYwfzgDe4zl zi#u2;FVKRkS7HShFIv2C(ZUCr8M9;=?kcd7i`gs@t9zwFtfA3Su$qurk-N}ekz+C= zs2U_H1~_O&7KVZcbCwg&k>3n&#si-`k6`DU3Yo*i*Hr*jJqFJ)34>T{ycwe;eA4NR4t zV#Ts$%PGs2Em^R=f4ZY<^1>xZ)mLBFSXo$9R6AQYmG zRKso~q@Bn)hr-p~ammCc<^a|+xi0c;YnfxWawWej5_}$7=H+v@Z0X{~JG)2QyH3#E z&W>f88mbEOO3U}JUHHJFhgNRf>C7UDeQ-b80)N#AI$hme-K@#(>+c`nHqg_>mzA@W zm+>59WiQBuQF(=BwHU(5v7V-qtqbp6vVJ$N_0ZuXO=VQ=V<&+J8H<{u^S1r>wwQL#ISEfzZ+=4?P4j z4=th4MVIITc(i2E1H1bN+Xl~FI)CX@Z*5a^>EY6{{H;qDEO=;)?pVzR~gFOy%xn z_bphnb6;NGfdfUg9X+kpFsqJ0g;G=7+*Gt>~w__hL}*HI=}((SHA=rbYnvz8pslVz|2>e zR}xPE6f&GG|DP3gT%xH?E-a$TwQIF>tCr)8R;++O4=we^RSMK%ioKJ40s4BgI5i#c8`MYq!jSF9K(`46v#*jZ+1h{>0J zyNe=Y7?Hsqku-kb;UjO8OwB#!JVFW+H1Kc9cl4n6(UI}-kSd7+WEYL2Z2noycS1uQ zgf0bq_G8qHRg+l>`%nup=ZAY|I8wBDxTZuyM7I+nEgeZvrjg)_);XJa6`{TmNg|jO zETTiAc!Nh=Y!qOFSPK_yZpn6yoMYj07BO8>SW;26YvURwIV@SVZsXSNJ9lp1b)cxS zG1KG5+XL)k+R@QgUD?1Stcv_{d``W|v4-Y`Ccd|}ACmQSwl>yOl#o$5QpCbIyjD}M zgC+F`m*2Z+HF~h%(7r?EO`WZ^&CCub)y7YAV{1qCuFYFnO`r`YV=-5_vi0OP5J9Ax zm?VP^Ttxlpc*`_{UgNTq+xk@;VA)C{)XiIW9yn6l($Ul3?^lv{*44C;7t253T9d-PCDoNx zk-Dg^E-J2L@?0BV0@%y9BsNzQO=;2(9WE%XYR>eHj`r5=Uv|%eHCy)==Iz^eu(YY8 zr7=TdkdkQww2q$oy{`O#SllIs96JH6FywJ@s*A9Ji1wu#L1xV?-1E#19 zzy^FG9>PxRcr@1t1%r(3&Pt6C*5{q~BSOyP4rhd12t&7QDRW4cE)5JoLX05NVj>dr zXb(O}YKAa!VZ07P#3##_R&D>gew3>FsW- ztKd5pnVOEAEvc+&?H?QIuGzQbo`ssh!+ZA}ENSk^v~`0k$Hs!EEU=OmBZboR9YBySe9_DQE;{q8B G~Z8!>c} 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

jHfkAY3-anAM35De0b*T6t7bRi%%CJjzR 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

>t^e2nT`P$&J7LVe>KdR5Q4lXeOX|8Q3 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 z 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 z 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

3<|5~%=m;i!V%5!t$J)%= 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 z

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%0Aw`!9Hy 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 zBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J- zkbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(T zKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU z0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb z1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ! z5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^ zNI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndv zAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnw zfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz z0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8< z2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|Drd zBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J- zkbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(T zKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU z0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb z1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ! z5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^ zNI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndv zAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnw zfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz z0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8< z2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|Drd zBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J- zkbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(T zKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU z0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb z1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ! z5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^ zNI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndv zAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnw zfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz z0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8< z2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|Drd zBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J- zkbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(T zKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU z0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb z1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ! z5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^ zNI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndv zAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnw zfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz z0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8< z2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|Drd zBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|DrdBp?9^NI(J- YkbndvAOQ(TKmrnwfCMD)og(o61AtnF*Z=?k literal 0 HcmV?d00001 diff --git a/test_input/ksa/doc/image/login-bg.jpg b/test_input/ksa/doc/image/login-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc0c83b76a0b4920609514271d78103a422de30f GIT binary patch literal 93868 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�NMg@HE1?p|kzfnKN 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 WF8Jd8t)dIQSn$RF0RuKO5B~wC01i9= literal 0 HcmV?d00001 diff --git "a/test_input/ksa/doc/\346\235\255\345\267\236\345\207\257\346\200\235\347\210\261\347\211\251\346\265\201\347\256\241\347\220\206\347\263\273\347\273\237\350\277\201\347\247\273\346\226\271\346\241\210.doc" "b/test_input/ksa/doc/\346\235\255\345\267\236\345\207\257\346\200\235\347\210\261\347\211\251\346\265\201\347\256\241\347\220\206\347\263\273\347\273\237\350\277\201\347\247\273\346\226\271\346\241\210.doc" new file mode 100644 index 0000000000000000000000000000000000000000..e8e7836616573b29a55c1b0aa3a8ee2166cd6433 GIT binary patch literal 29747 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 + 4.0.0 + + + com.ksa + ksa-root + 3.9.0 + + + ksa-core + jar + + ksa-core + 杭州凯思爱物流管理系统 - 核心模块 + + + UTF-8 + + + + + org.springframework + spring-core + + + org.springframework + spring-context + + + commons-lang + commons-lang + + + + + com.ksa + ksa-debug + 3.9.0 + + + + + org.freemarker + freemarker + 2.3.18 + test + + + diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ContextException.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ContextException.java new file mode 100644 index 0000000..c0af239 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ContextException.java @@ -0,0 +1,57 @@ +package com.ksa.context; + +public class ContextException extends RuntimeException { + + private static final long serialVersionUID = 7219785174471312237L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + */ + public ContextException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + * + * @param message + * the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + public ContextException( String message ) { + super( message ); + } + + /** + * Constructs a new exception with the specified detail message and cause. + *

+ * Note that the detail message associated with cause is not automatically incorporated in this + * exception's detail message. + * + * @param message + * the detail message (which is saved for later retrieval by the {@link #getMessage()} method). + * @param cause + * the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null value is + * permitted, and indicates that the cause is nonexistent or unknown.) + * @since v0.0.1 + */ + public ContextException( String message, Throwable cause ) { + super( message, cause ); + } + + /** + * Constructs a new exception with the specified cause and a detail message of + * (cause==null ? null : cause.toString()) (which typically contains the class and detail message of cause + * ). This constructor is useful for exceptions that are little more than wrappers for other throwables (for example, + * {@link java.security.PrivilegedActionException}). + * + * @param cause + * the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null value is + * permitted, and indicates that the cause is nonexistent or unknown.) + * @since v0.0.1 + */ + public ContextException( Throwable cause ) { + super( cause ); + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContext.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContext.java new file mode 100644 index 0000000..da174c6 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContext.java @@ -0,0 +1,45 @@ +package com.ksa.context; + + +public interface ServiceContext { + /** + * 根据服务名称获取一个业务服务。 + * + * @param name 服务名称 + * @return 业务服务对象 + * @throws ContextException + * 所需业务服务不存在 + */ + Object getService( String name ) throws ContextException; + + /** + * 根据服务名称获取特定类型的一个业务服务。 + * + * @param name + * 服务名称 + * @param requiredType + * 服务类型 + * @return 业务服务对象 + * @throws ContextException + * 业务服务获取不存在或类型不符合要求 + */ + T getService( String name, Class requiredType ) throws ContextException; + + /** + * 获取特定类型的一个业务服务。 + * + * @param requiredType 服务类型 + * @return 业务服务对象 + * @throws ContextException + * 特定类型的业务服务不存在 + */ + T getService( Class requiredType ) throws ContextException; + + /** + * 判断特定名称的服务是否存在。 + * + * @param name 服务名称 + * @return true 对应名称的服务存在;否则返回 false + */ + boolean containsService( String name ); +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContextUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContextUtils.java new file mode 100644 index 0000000..c1c66b4 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/ServiceContextUtils.java @@ -0,0 +1,72 @@ +package com.ksa.context; + +/** + * 业务逻辑服务配置管理工具。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class ServiceContextUtils { + + private static ServiceContext instance; + + /** + * 初始化业务服务配置实例。 + */ + public static void init( ServiceContext instance ) { + ServiceContextUtils.instance = instance; + } + + /** + * 根据服务名称获取一个业务服务。 + * + * @param name + * 服务名称 + * @return 业务服务对象 + * @throws ContextException + * 所需业务服务不存在 + */ + public static Object getService( String name ) throws ContextException { + return instance.getService( name ); + } + + /** + * 根据服务名称获取特定类型的一个业务服务。 + * + * @param name + * 服务名称 + * @param requiredType + * 服务类型 + * @return 业务服务对象 + * @throws ContextException + * 业务服务获取不存在或类型不符合要求 + */ + public static T getService( String name, Class requiredType ) throws ContextException { + return instance.getService( name, requiredType ); + } + + /** + * 获取特定类型的一个业务服务。 + * + * @param requiredType + * 服务类型 + * @return 业务服务对象 + * @throws ContextException + * 特定类型的业务服务不存在 + */ + public static T getService( Class requiredType ) throws ContextException { + return instance.getService( requiredType ); + } + + /** + * 判断特定名称的服务是否存在。 + * + * @param name + * 服务名称 + * @return true 对应名称的服务存在;否则返回 false + */ + public static boolean containsService( String name ) { + return instance.containsService( name ); + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/context/spring/SpringServiceContext.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/spring/SpringServiceContext.java new file mode 100644 index 0000000..5fb4582 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/context/spring/SpringServiceContext.java @@ -0,0 +1,49 @@ +package com.ksa.context.spring; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; + +import com.ksa.context.ContextException; +import com.ksa.context.ServiceContext; + +public class SpringServiceContext implements ServiceContext { + + protected ApplicationContext context; + + public SpringServiceContext( ApplicationContext context ) { + this.context = context; + } + + @Override + public Object getService( String name ) throws ContextException { + try { + return this.context.getBean( name ); + } catch( BeansException ex ) { + throw new ContextException( ex ); + } + } + + @Override + public T getService( String name, Class requiredType ) throws ContextException { + try { + return this.context.getBean( name, requiredType ); + } catch( BeansException ex ) { + throw new ContextException( ex ); + } + } + + @Override + public T getService( Class requiredType ) throws ContextException { + try { + return this.context.getBean( requiredType ); + } catch( BeansException ex ) { + throw new ContextException( ex ); + } + } + + @Override + public boolean containsService( String name ) { + return this.context.containsBean( name ); + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/AbstractQueryClause.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/AbstractQueryClause.java new file mode 100644 index 0000000..2b00cdd --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/AbstractQueryClause.java @@ -0,0 +1,95 @@ +package com.ksa.dao; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.util.StringUtils; + +public abstract class AbstractQueryClause implements QueryClause { + + private static final Logger logger = LoggerFactory.getLogger( AbstractQueryClause.class ); + + protected String columnName; + + public AbstractQueryClause( String columnName ) { + this.columnName = columnName; + } + + /** + * 返回查询条件子句中所需的字段名称。 + * + * @return 字段列名 + */ + protected String getColumnName() { + return columnName; + } + + /** + * 将字符串格式的值,解析为 SQL 条件语句右侧的合理值。默认为处理字符串中的单引号 "'" 字符。 + * + * @param value + * @return + */ + protected String getParsedValue( String value ) throws Exception { + StringBuilder sb = new StringBuilder( 64 ); + if( StringUtils.hasText( value ) ) { + sb.append( " '" ).append( value.replace( "'", "\'" ) ).append( "' " ); // 格式化字符串 + } else { + sb.append( " '' OR " ) + .append( getColumnName() ) + .append( " IS NULL " ); + } + return sb.toString(); + } + + /*** + * 获取比较操作的具体比较符号,默认为 " = ",也就是相等比较。 + * @return + */ + protected String getCompareSign() { + return " = "; + } + + /** + * 获取单个查询参数所需的比较判断语句。 + * + * @param value + * 查询比较的参数 + * @param index + * 参数所在参数数据的索引 + * @return SQL WHER 子句所需的比较判断语句 + */ + protected String getCompareCondition( String value, int index ) { + try { + StringBuilder sb = new StringBuilder( 64 ); + sb.append( " ( " ) + .append( getColumnName() ) + .append( getCompareSign() ) + .append( getParsedValue( value ) ) + .append( " ) " ); + return sb.toString(); + } catch( Exception e ) { + logger.warn( "获取查询比较片段时发生异常,忽略此比较。", e ); + } + return null; + } + + @Override + public Collection compute( String[] values ) { + if( values == null || values.length <= 0 ) { + return Collections.emptyList(); + } + Collection result = new ArrayList( values.length ); + for( int i = 0; i < values.length; i++ ) { + String condition = getCompareCondition( values[i], i ); + if( StringUtils.hasText( condition ) ) { + result.add( condition ); + } + } + return result; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/DateQueryClause.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/DateQueryClause.java new file mode 100644 index 0000000..0502d01 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/DateQueryClause.java @@ -0,0 +1,61 @@ +package com.ksa.dao; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.util.StringUtils; + +public class DateQueryClause extends AbstractQueryClause implements QueryClause { + + private static final Logger logger = LoggerFactory.getLogger( DateQueryClause.class ); + + public DateQueryClause( String columnName ) { + super( columnName ); + } + + @Override + protected String getParsedValue( String value ) throws Exception { + DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + Date date = null; + try { + date = format.parse( value ); + } catch (ParseException ex) { + value = value + "-01"; + try { date = format.parse( value ); } + catch( ParseException e ) { + logger.warn( "解析日期字符串时发生异常。", e ); + throw e; + } + } + StringBuilder sb = new StringBuilder( 16 ); + sb.append( " '" ) + .append( format.format( date ) ) + .append( "' " ); + return sb.toString(); + } + + @Override + protected String getCompareCondition( String value, int index ) { + if( ! StringUtils.hasText( value ) ) { + return null; + } + try { + StringBuilder sb = new StringBuilder( 64 ); + sb.append( " ( " ) + .append( getColumnName() ) + .append( ( index % 2 == 0 ) ? " >= " : " <= " ) + .append( getParsedValue( value ) ) + .append( " ) " ); + return sb.toString(); + } catch( Exception e ) { + logger.warn( "获取查询比较片段时发生异常,忽略此比较。", e ); + } + return null; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/MultiIdsQueryClause.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/MultiIdsQueryClause.java new file mode 100644 index 0000000..9b3e744 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/MultiIdsQueryClause.java @@ -0,0 +1,31 @@ +package com.ksa.dao; + +import java.util.Collection; + +public class MultiIdsQueryClause extends AbstractQueryClause implements QueryClause { + + public MultiIdsQueryClause(String columnName) { + super(columnName); + } + + @Override + public Collection compute(String[] values) { + Collection clauses = super.compute( values ); + if( clauses != null && clauses.size() > 0 ) { + StringBuilder sb = new StringBuilder( 16 * clauses.size() ); + sb.append( " ( " ); + int i = 0; + for( String clause : clauses ) { + if( i++ > 0 ) { + sb.append( " OR " ); + } + sb.append( clause ); + } + sb.append( " ) " ); + clauses.clear(); + clauses.add( sb.toString() ); + } + return clauses; + }; + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/QueryClause.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/QueryClause.java new file mode 100644 index 0000000..fb29a04 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/QueryClause.java @@ -0,0 +1,20 @@ +package com.ksa.dao; + +import java.util.Collection; + +/** + * 符合查询条件生成统一接口。用于将传入的查询参数转换为 SQL 语句中 WHERE 子句所需的查询格式。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface QueryClause { + + /** + * 将每个参数转换为 WHERE 子句中的查询片段。 + * @param values 参数数组 + * @return SQL WHERE 子句的查询片段数组 + */ + Collection compute( String[] values ); +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/TextQueryClause.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/TextQueryClause.java new file mode 100644 index 0000000..04b18ce --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/TextQueryClause.java @@ -0,0 +1,29 @@ +package com.ksa.dao; + +import com.ksa.dao.QueryClause; +import com.ksa.util.StringUtils; + +public class TextQueryClause extends AbstractQueryClause implements QueryClause { + + public TextQueryClause( String columnName ) { + super( columnName ); + } + + @Override + protected String getCompareSign() { + return " LIKE "; + } + + @Override + protected String getParsedValue( String value ) { + StringBuilder sb = new StringBuilder( 64 ); + if( StringUtils.hasText( value ) ) { + sb.append( " '%" ).append( value.replace( "'", "''" ) ).append( "%' " ); // 格式化字符串 + } else { + sb.append( " '' OR " ) + .append( getColumnName() ) + .append( " IS NULL " ); + } + return sb.toString(); + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/BasicDataDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/BasicDataDao.java new file mode 100644 index 0000000..35be528 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/BasicDataDao.java @@ -0,0 +1,23 @@ +package com.ksa.dao.bd; + +import java.util.List; + +import com.ksa.model.bd.BasicData; + +/** + * 基础数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface BasicDataDao { + + int insertBasicData( BasicData data ) throws RuntimeException; + int updateBasicData( BasicData data ) throws RuntimeException; + int deleteBasicData( BasicData data ) throws RuntimeException; + + List selectAllBasicData() throws RuntimeException; + List selectBasicDataByType( String typeId ) throws RuntimeException; + BasicData selectBasicDataById( String id ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/CurrencyRateDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/CurrencyRateDao.java new file mode 100644 index 0000000..1eb45d0 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/CurrencyRateDao.java @@ -0,0 +1,121 @@ +package com.ksa.dao.bd; + +import java.util.Date; +import java.util.List; + +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.CurrencyRate; + +/** + * 结算货币数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface CurrencyRateDao { + + int insertRate( CurrencyRate rate ) throws RuntimeException; + int updateRate( CurrencyRate rate ) throws RuntimeException; + int deleteRate( CurrencyRate rate ) throws RuntimeException; + + /** + * 获取所有结算货币。 + * @return 所有结算货币的列表 + * @throws RuntimeException + */ + List selectAllCurrency() throws RuntimeException; + + /** + * 获取特定货币。 + * @param id 给定的货币标识 + * @return 返回给定标识的货币。 + * @throws RuntimeException + */ + Currency selectCurrencyById( String id ) throws RuntimeException; + + /** + * 获取当前所有货币的最新汇率。 + * @return + * @throws RuntimeException 数据获取失败 + */ + List selectLatestRates() throws RuntimeException; + + /** + * 获取距离 date(包含) 最近的所有货币汇率。 + * @param date 限定日期 + * @return + * @throws RuntimeException 数据获取失败 + */ + List selectLatestRates( Date date ) throws RuntimeException; + + /** + * 获取特定货币最近的汇率(按记录汇率的日期倒序排列的第一条记录),如果没有记录过汇率则返回 null 。 + * @param currencyId 货币类型标识 + * @return 特定货币最近的汇率,如果不存在则返回 null 。 + * @throws RuntimeException + */ + CurrencyRate selectLatestRate( String currencyId ) throws RuntimeException; + + /** + * 获取特定货币具体日期之前(包含)最近的汇率(按记录汇率的日期倒序排列的小于等于给定日期的第一条记录), + * 如果没有记录过汇率则返回 null 。 + * @param currencyId 货币类型标识 + * @param date 限定日期 + * @return 特定货币最近的汇率,如果不存在则返回 null 。 + * @throws RuntimeException + */ + CurrencyRate selectLatestRate( String currencyId, Date date ) throws RuntimeException; + + /** + * 获取某一日期范围内的货币汇率信息。 + * @param start 开始日期(包含) + * @param end 结束日期(不包含) + * @return + * @throws RuntimeException 数据获取失败 + */ + List selectRateByDate( Date start, Date end ) throws RuntimeException; + + /** + * 获取具体货币某一日期范围内的汇率信息。 + * @param start 开始日期(包含)标识 + * @param end 结束日期(不包含) + * @param currencyId 货币类型 + * @return + * @throws RuntimeException 数据获取失败 + */ + List selectRateByDate( Date start, Date end, String currencyId ) throws RuntimeException; + + /** + * 获取某一用户特有的结算货币汇率信息。 + * @param customerId 客户标识 + * @return + * @throws RuntimeException + */ + List selectRateByPartner( String customerId ) throws RuntimeException; + + /** + * 获取具体货币某一用户特有的结算货币汇率信息。 + * @param customerId 客户标识 + * @param currencyId 货币类型标识 + * @return + * @throws RuntimeException + */ + CurrencyRate selectRateByPartner( String customerId, String currencyId ) throws RuntimeException; + + /** + * 通过唯一标识获取汇率数据。 + * @param id 汇率数据的标识 + * @return + * @throws RuntimeException + */ + CurrencyRate selectRateById( String id ) throws RuntimeException; + + /** + * 获取所有汇率设置。 + * @return + * @throws RuntimeException + */ + List selectAllRates() throws RuntimeException; + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/PartnerDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/PartnerDao.java new file mode 100644 index 0000000..ac3a331 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/bd/PartnerDao.java @@ -0,0 +1,25 @@ +package com.ksa.dao.bd; + +import com.ksa.model.bd.Partner; +import com.ksa.model.bd.PartnerType; + +/** + * 合作伙伴数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface PartnerDao { + int insertPartner( Partner partner ) throws RuntimeException; + int updatePartner( Partner data ) throws RuntimeException; + int deletePartner( Partner data ) throws RuntimeException; + Partner selectPartnerById( String id ) throws RuntimeException; + Partner selectPartnerByCode( String id ) throws RuntimeException; + + int insertPartnerType( Partner partner, PartnerType type ) throws RuntimeException; + int deletePartnerType( Partner partner, PartnerType type ) throws RuntimeException; + + int insertPartnerExtra( Partner partner, String extra ) throws RuntimeException; + int deletePartnerExtra( Partner partner, String extra ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountCurrencyRateDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountCurrencyRateDao.java new file mode 100644 index 0000000..a9da7dd --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountCurrencyRateDao.java @@ -0,0 +1,21 @@ +package com.ksa.dao.finance; + +import java.util.List; + +import com.ksa.dao.bd.CurrencyRateDao; +import com.ksa.model.finance.AccountCurrencyRate; + +/** + * 基于结算单的货币汇率 DAO + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface AccountCurrencyRateDao extends CurrencyRateDao { + + int insertRate( AccountCurrencyRate rate ) throws RuntimeException; + int updateRate( AccountCurrencyRate rate ) throws RuntimeException; + int deleteRate( AccountCurrencyRate rate ) throws RuntimeException; + List selectRatesByAccountId( String accountId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountDao.java new file mode 100644 index 0000000..c0dbaef --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/AccountDao.java @@ -0,0 +1,28 @@ +package com.ksa.dao.finance; + +import java.util.List; + +import com.ksa.model.finance.Account; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; + +/** + * 费用结算对账单数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface AccountDao { + int insertAccount( Account account ) throws RuntimeException; + int insertAccountCharges( Account account, List charges ) throws RuntimeException; + int deleteAccountCharges( Account account, List charges ) throws RuntimeException; + int updateAccount( Account account ) throws RuntimeException; + int updateAccountState( Account account ) throws RuntimeException; + int deleteAccount( Account account ) throws RuntimeException; + int querySimilarAccountCodeCount( String code ) throws RuntimeException; + + Account selectAccountById( String id ) throws RuntimeException; + /** 获取结算单相关的业务托单信息 */ + List selectBookingNoteByAccountId( String accountId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/ChargeDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/ChargeDao.java new file mode 100644 index 0000000..02f5a47 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/ChargeDao.java @@ -0,0 +1,23 @@ +package com.ksa.dao.finance; + +import java.util.List; + +import com.ksa.model.finance.Charge; + +/** + * 费用数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface ChargeDao { + int insertCharge( Charge charge ) throws RuntimeException; + int updateCharge( Charge charge ) throws RuntimeException; + int deleteCharge( Charge charge ) throws RuntimeException; + + Charge selectChargeById( String id ) throws RuntimeException; + List selectChargeByBookingNoteId( String bnId ) throws RuntimeException; + List selectChargeByBookingNoteId( String bnId, int direction, int nature ) throws RuntimeException; + List selectChargeByAccountId( String accountId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/InvoiceDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/InvoiceDao.java new file mode 100644 index 0000000..d586cb0 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/finance/InvoiceDao.java @@ -0,0 +1,23 @@ +package com.ksa.dao.finance; + +import java.util.List; + +import com.ksa.model.finance.Invoice; + +/** + * 费用数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface InvoiceDao { + int insertInvoice( Invoice invoice ) throws RuntimeException; + int updateInvoice( Invoice invoice ) throws RuntimeException; + int deleteInvoice( Invoice invoice ) throws RuntimeException; + + int updateInvoiceAccount( Invoice invoice ) throws RuntimeException; + + Invoice selectInvoiceById( String id ) throws RuntimeException; + List selectInvoiceByAccountId( String accountId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/AbstractLogisticsModelDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/AbstractLogisticsModelDao.java new file mode 100644 index 0000000..1260364 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/AbstractLogisticsModelDao.java @@ -0,0 +1,13 @@ +package com.ksa.dao.logistics; + +import com.ksa.model.logistics.BaseLogisticsModel; + + +public interface AbstractLogisticsModelDao { + + int insertLogisticsModel( T model ) throws RuntimeException; + int updateLogisticsModel( T model ) throws RuntimeException; + int deleteLogisticsModel( T model ) throws RuntimeException; + T selectLogisticsModelById( String id ) throws RuntimeException; + T selectLogisticsModelByBookingNoteId( String bookingNoteId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ArrivalNoteDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ArrivalNoteDao.java new file mode 100644 index 0000000..cb5559d --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ArrivalNoteDao.java @@ -0,0 +1,8 @@ +package com.ksa.dao.logistics; + +import com.ksa.model.logistics.ArrivalNote; + + +public interface ArrivalNoteDao extends AbstractLogisticsModelDao { + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BillOfLadingDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BillOfLadingDao.java new file mode 100644 index 0000000..bc6d483 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BillOfLadingDao.java @@ -0,0 +1,8 @@ +package com.ksa.dao.logistics; + +import com.ksa.model.logistics.BillOfLading; + + +public interface BillOfLadingDao extends AbstractLogisticsModelDao { + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteCargoDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteCargoDao.java new file mode 100644 index 0000000..6a1d4cb --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteCargoDao.java @@ -0,0 +1,20 @@ +package com.ksa.dao.logistics; + +import java.util.List; + +import com.ksa.model.logistics.BookingNoteCargo; + +/** + * 货物数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface BookingNoteCargoDao { + int insertCargo( BookingNoteCargo cargo ) throws RuntimeException; + int updateCargo( BookingNoteCargo cargo ) throws RuntimeException; + int deleteCargo( BookingNoteCargo cargo ) throws RuntimeException; + BookingNoteCargo selectCargoById( String id ) throws RuntimeException; + List selectCargoByBookingNoteId( String noteId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteDao.java new file mode 100644 index 0000000..d4ecae5 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/BookingNoteDao.java @@ -0,0 +1,29 @@ +package com.ksa.dao.logistics; + +import com.ksa.model.logistics.BookingNote; + +/** + * 托单数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface BookingNoteDao { + int insertBookingNote( BookingNote note ) throws RuntimeException; + int updateBookingNote( BookingNote note ) throws RuntimeException; + int updateBookingNoteState( BookingNote note ) throws RuntimeException; + int updateBookingNoteType( BookingNote note ) throws RuntimeException; + int updateBookingNoteChargeDate( BookingNote note ) throws RuntimeException; + int deleteBookingNote( BookingNote note ) throws RuntimeException; + BookingNote selectBookingNoteById( String id ) throws RuntimeException; + /** + * 通过提单号查询相关的托单 + * @param note + * @return + * @throws RuntimeException + */ + BookingNote selectBookingNoteByLading( BookingNote note ) throws RuntimeException; + int selectBookingNoteCount() throws RuntimeException; + int selectBookingNoteCount( String queryString ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ManifestDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ManifestDao.java new file mode 100644 index 0000000..9bc53d5 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/ManifestDao.java @@ -0,0 +1,8 @@ +package com.ksa.dao.logistics; + +import com.ksa.model.logistics.Manifest; + + +public interface ManifestDao extends AbstractLogisticsModelDao { + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseBookingDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseBookingDao.java new file mode 100644 index 0000000..4a04e3f --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseBookingDao.java @@ -0,0 +1,8 @@ +package com.ksa.dao.logistics; + +import com.ksa.model.logistics.WarehouseBooking; + + +public interface WarehouseBookingDao extends AbstractLogisticsModelDao { + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseNotingDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseNotingDao.java new file mode 100644 index 0000000..3b7d54c --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/logistics/WarehouseNotingDao.java @@ -0,0 +1,8 @@ +package com.ksa.dao.logistics; + +import com.ksa.model.logistics.WarehouseNoting; + + +public interface WarehouseNotingDao extends AbstractLogisticsModelDao { + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/PermissionDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/PermissionDao.java new file mode 100644 index 0000000..620e63d --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/PermissionDao.java @@ -0,0 +1,38 @@ +package com.ksa.dao.security; + +import java.util.List; + +import com.ksa.model.security.Permission; + +/** + * 权限数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface PermissionDao { + + /** + * 从数据存储库中读取所有权限数据。 + * @return 所有权限数据列表 + * @throws RuntimeException 数据操作异常 + */ + List selectAllPermission() throws RuntimeException; + + /** + * 通过权限标识从数据存储库中查询权限数据。 + * @param id 权限标识 + * @return 当特定标识的权限存在时返回对应的权限模型,否则返回 null + * @throws RuntimeException 数据操作异常 + */ + Permission selectPermissionById( String id ) throws RuntimeException; + + /** + * 通过角色标识从数据存储库中查询角色所具有的权限数据。 + * @param roleId 角色标识 + * @return 角色所含权限数据列表 + * @throws RuntimeException 数据操作异常 + */ + List selectPermissionByRoleId( String roleId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/RoleDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/RoleDao.java new file mode 100644 index 0000000..7141ef5 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/RoleDao.java @@ -0,0 +1,72 @@ +package com.ksa.dao.security; + +import java.util.List; + +import com.ksa.model.security.Role; + +/** + * 角色数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface RoleDao { + + /** + * 向数据存储库中插入一个角色数据。 + * @param role 角色模型 + * @return 插入操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int insertRole( Role role ) throws RuntimeException; + + /** + * 从数据存储库中更新一个角色数据。 + * @param role 角色模型 + * @return 更新操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int updateRole( Role role ) throws RuntimeException; + + /** + * 从数据存储库中删除一个角色数据。 + * @param role 角色模型 + * @return 删除操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int deleteRole( Role role ) throws RuntimeException; + + /** + * 从数据存储库中读取所有角色数据。 + * @return 所有角色模型列表 + * @throws RuntimeException 数据操作异常 + */ + List selectAllRole() throws RuntimeException; + + /** + * 通过角色标识从数据存储库中查询角色数据。 + * @param id 角色标识 + * @return 当特定标识的角色存在时返回对应的角色模型,否则返回 null + * @throws RuntimeException 数据操作异常 + */ + Role selectRoleById( String id ) throws RuntimeException; + + /** + * 向数据存储库中插入一个角色的权限。 + * @param roleId 角色标识 + * @param permissionId 权限标识 + * @return 插入操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int insertRolePermission( String roleId, String permissionId ) throws RuntimeException; + + /** + * 从数据存储库中删除一个角色的权限。 + * @param roleId 角色标识 + * @param permissionId 权限标识 + * @return 删除操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int deleteRolePermission( String roleId, String permissionId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/UserDao.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/UserDao.java new file mode 100644 index 0000000..21c5541 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/dao/security/UserDao.java @@ -0,0 +1,72 @@ +package com.ksa.dao.security; + +import java.util.List; + +import com.ksa.model.security.User; + +/** + * 用户数据访问接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface UserDao { + + /** + * 向数据存储库中插入一个用户数据。 + * @param user 用户模型 + * @return 插入操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int insertUser( User user ) throws RuntimeException; + + /** + * 从数据存储库中更新一个用户数据。 + * @param user 用户模型 + * @return 更新操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int updateUser( User user ) throws RuntimeException; + + /** + * 从数据存储库中删除一个用户数据。 + * @param user 用户模型 + * @return 删除操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int deleteUser( User user ) throws RuntimeException; + + /** + * 从数据存储库中读取所有用户数据。 + * @return 所有用户模型列表 + * @throws RuntimeException 数据操作异常 + */ + List selectAllUser() throws RuntimeException; + + /** + * 通过用户标识从数据存储库中查询用户数据。 + * @param id 用户标识 + * @return 当特定标识的用户存在时返回对应的用户模型,否则返回 null + * @throws RuntimeException 数据操作异常 + */ + User selectUserById( String id ) throws RuntimeException; + + /** + * 向数据存储库中插入一个用户的角色。 + * @param userId 用户标识 + * @param roleId 角色标识 + * @return 插入操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int insertUserRole( String userId, String roleId ) throws RuntimeException; + + /** + * 从数据存储库中删除一个用户的角色。 + * @param userId 用户标识 + * @param roleId 角色标识 + * @return 删除操作影响的数据数量 + * @throws RuntimeException 数据操作异常 + */ + int deleteUserRole( String userId, String roleId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/BaseModel.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/BaseModel.java new file mode 100644 index 0000000..1c100ce --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/BaseModel.java @@ -0,0 +1,41 @@ +package com.ksa.model; + +import java.io.Serializable; + +import org.apache.commons.lang.builder.ToStringBuilder; + +/** + * 业务数据模型抽象基类。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public abstract class BaseModel implements Serializable { + + private static final long serialVersionUID = 1484025868680981064L; + + /** 模型标识 */ + protected String id; + + /** + * 获取模型标识。 + * @return 模型标识 + */ + public String getId() { + return id; + } + + /** + * 设置模型标识。 + * @param id 模型标识 + */ + public void setId( String id ) { + this.id = id; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString( this ); + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelState.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelState.java new file mode 100644 index 0000000..4c8068e --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelState.java @@ -0,0 +1,122 @@ +package com.ksa.model; + + +/** + * 业务数据模型的基本状态常量描述。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class ModelState { + + /** 表示业务对象没有任何状态,或者可以认为其处于最原始状态。*/ + public static final int NONE = 0x0; + /** 表示业务对象状态为 处理中。*/ + public static final int PROCESSING = 0x1; + /** 表示业务对象状态为 审核中。*/ + public static final int CHECKING = 0x2; + /** 表示业务对象状态为 审核通过。*/ + public static final int CHECKED = 0x8; + + /** + * 判断业务对象当前状态是否为 NONE 。 + * @param state 业务模型的状态 + * @return + */ + public static boolean isNone( int state ) { + return state == NONE; + } + + /** + * 将当前状态设置为空状态。 + * @param state 待设置状态 + * @return 空状态 + */ + public static int setNone( int state ) { + return 0; + } + + /** + * 判断业务对象状态是否为 处理中。 + * @param state 待判断状态 + */ + public static boolean isProcessing( int state ) { + return ( PROCESSING & state ) > 0; + } + /** + * 将当前状态设置为 处理中。 + * @param state 待设置状态 + * @return 表示 处理中 的状态 + */ + public static int setProcessing( int state ) { + return state | PROCESSING; + } + + /** + * 将当前状态设置为 非处理中。 + * @param state 待设置状态 + * @return 表示 非处理中 的状态 + */ + public static int setUnprocessing( int state ) { + return state & ~PROCESSING ; + } + + /** + * 判断业务对象状态是否为 审核中。 + * @param state 待判断状态 + */ + public static boolean isChecking( int state ) { + return ( CHECKING & state ) > 0; + } + + /** + * 判断业务对象状态是否为 审核通过。 + * @param state 待判断状态 + */ + public static boolean isChecked( int state ) { + return ( CHECKED & state ) > 0; + } + + /** + * 将当前状态设置为 审核中。 + * @param state 待设置状态 + * @return 表示 审核中 的状态 + */ + public static int setChecking( int state ) { + return state | CHECKING; + } + + /** + * 将当前状态设置为 非审核中。 + * @param state 待设置状态 + * @return 表示 非审核中 的状态 + */ + public static int setUnchecking( int state ) { + return state & ~CHECKING ; + } + + /** + * 将当前状态设置为 审核通过。 + * @param state 待设置状态 + * @return 表示 审核通过 的状态 + */ + public static int setChecked( int state ) { + return state | CHECKED; + } + + /** + * 将当前状态设置为 未审核通过。 + * @param state 待设置状态 + * @return 表示 未审核通过 的状态 + */ + public static int setUnchecked( int state ) { + return state & ~CHECKED; + } + + /** + * 构造函数。
+ * 状态常量可以通过继承的方式进行扩展,但是类本身不允许实例化。 + */ + protected ModelState() { } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelUtils.java new file mode 100644 index 0000000..7e1a1de --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/ModelUtils.java @@ -0,0 +1,88 @@ +package com.ksa.model; + +import java.util.UUID; + +import com.ksa.util.StringUtils; + + +/** + * 业务模型工具类。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class ModelUtils { + + /** 纯工具类,禁止实例化。 */ + private ModelUtils() {} + + /** + * 新建业务对象的标识。 + */ + public static final String FRESH_OBJECT_ID = ""; + + /** + * 随机生成业务对象的唯一标识。 + * + * @return 唯一标识。 + */ + public static String generateRandomId() { + return UUID.randomUUID().toString(); + } + + /* --------------------- 业务对象状态判断函数 --------------------- */ + + /** + * 判断业务对象是否未被持久化(即:是一个新建对象)。 + *

+ * 对象被持久化表示对象在数据库等数据存储区存在副本。 + * + * @param obj + * 业务对象 + * @return 如果业务对象是一个新建对象未被持久化,则返回 true;否则返回 false。 + */ + public static boolean isFreshObject( BaseModel obj ) { + return ! StringUtils.hasText( obj.getId() ); + } + + /** + * 判断业务对象是否已被持久化。 + *

+ * 对象被持久化表示对象在数据库等数据存储区存在副本。 + * + * @param obj + * 业务对象 + * @return 如果业务对象已经被持久化,则返回 true;否则返回 false。 + */ + public static boolean isPersistentObject( BaseModel obj ) { + return !isFreshObject( obj ); + } + + /** + * 判断业务对象是否为系统保留对象。 + *

+ * 系统保留对象时处理业务逻辑时必不可少的对象,不能对其进行修改。 + *

+ * 当前默认实现为对业务对象的 id 进行判断,如果是系统保留对象,则其 id 为特定意义的唯一标识; 否则对象 id 是通过 generateRandomId 方法随机生成的 UUID 字符串。 + * + * @param obj + * 业务对象 + * @return 如果业务对象是系统保留对象,则返回 true;否则返回 false。 + */ + public static boolean isReservedObject( BaseModel obj ) { + if( obj == null ) { + return false; + } + String id = obj.getId(); + try { + UUID.fromString( id ); + return false; + } catch( IllegalArgumentException e ) { + // 不是uuid标识,说明是系统保留对象。 + return true; + } + + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicData.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicData.java new file mode 100644 index 0000000..7a1d094 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicData.java @@ -0,0 +1,104 @@ +package com.ksa.model.bd; + +import com.ksa.model.BaseModel; + +/** + * 基础数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class BasicData extends BaseModel { + + private static final long serialVersionUID = -657250627734478396L; + + /** 基础数据编码 */ + protected String code; + /** 基础数据名称 */ + protected String name; + /** 基础数据别名 */ + protected String alias; + /** 基础数据备注 */ + protected String note; + /** 基础数据类型 */ + protected BasicDataType type; + + /** 排序 */ + protected int rank; + + /** 额外属性 */ + protected String extra; + + public BasicData() { + + } + + public BasicData( String id ) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } + + public String getAlias() { + return alias; + } + + public void setAlias( String alias ) { + this.alias = alias; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + public BasicDataType getType() { + if( type == null ) { + type = new BasicDataType(); + } + return type; + } + + public void setType( BasicDataType type ) { + this.type = type; + } + + public String getExtra() { + return extra; + } + + public void setExtra( String extra ) { + this.extra = extra; + } + + public int getRank() { + return rank; + } + + public void setRank( int rank ) { + this.rank = rank; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicDataType.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicDataType.java new file mode 100644 index 0000000..fb1e0b5 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/BasicDataType.java @@ -0,0 +1,104 @@ +package com.ksa.model.bd; + +import com.ksa.model.BaseModel; + +/** + * 基础数据分类模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class BasicDataType extends BaseModel { + + private static final long serialVersionUID = -7778666427563739441L; + /** 币种 */ + public static final BasicDataType CURRENCY = new NativeBasicDataType( "00-currency", "结算货币" ); + /** 数量单位 */ + public static final BasicDataType UNITS = new NativeBasicDataType( "01-units", "数量单位" ); + /** 车辆类型 */ + public static final BasicDataType VEHICLE = new NativeBasicDataType( "08-vehicle", "车辆类型" ); + /** 报关类型 */ + //public static final BasicDataType CUSTOM = new NativeBasicDataType( "09-custom", "报关类型" ); + /** 费用类型 */ + public static final BasicDataType CHARGE = new NativeBasicDataType( "10-charge", "费用类型" ); + /** 贸易条款 */ + //public static final BasicDataType TRADE_CLAUSE = new NativeBasicDataType( "11-trade-clause", "贸易条款" ); + /** 运费条款 */ + //public static final BasicDataType FREIGHT_CLAUSE = new NativeBasicDataType( "12-freight-clause", "运费条款" ); + /** 附加费条款 */ + //public static final BasicDataType SURCHARGE_CLAUSE = new NativeBasicDataType( "13-surcharge-clause", "附加费条款" ); + /** 来往单位类型 */ + public static final BasicDataType DEPARTMENT = new NativeBasicDataType( "20-department", "来往单位类型" ); + /** 国家地区 */ + public static final BasicDataType STATE = new NativeBasicDataType( "30-state", "国家地区" ); + /** 海运港口 */ + public static final BasicDataType PORT_SEA = new NativeBasicDataType( "31-port-sea", "海运港口" ); + /** 空运港口 */ + public static final BasicDataType PORT_AIR = new NativeBasicDataType( "32-port-air", "空运港口" ); + /** 海运航线 */ + public static final BasicDataType ROUTE_SEA = new NativeBasicDataType( "33-route-sea", "海运航线" ); + /** 空运航线 */ + public static final BasicDataType ROUTE_AIR = new NativeBasicDataType( "34-route-air", "空运航线" ); + + /** 所有的基础数据类型 */ + public static final BasicDataType[] ALL_TYPE = new BasicDataType[] { + CURRENCY, + UNITS, + VEHICLE, + // CUSTOM, + CHARGE, + // TRADE_CLAUSE, + // FREIGHT_CLAUSE, + // SURCHARGE_CLAUSE, + DEPARTMENT, + STATE, + PORT_SEA, + PORT_AIR, + ROUTE_SEA, + ROUTE_AIR + }; + + public BasicDataType() { + + } + + public BasicDataType( String id, String name ) { + this.id = id; + this.name = name; + } + + /** 类型名称 */ + protected String name; + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + private static class NativeBasicDataType extends BasicDataType { + + private static final long serialVersionUID = -1209493492764946816L; + + public NativeBasicDataType( String id, String name ) { + this.id = id; + this.name = name; + } + @Override + public void setId( String id ) { + throw new UnsupportedOperationException( "不能修改系统保留基本数据类型的标识。" ); + } + @Override + public void setName( String name ) { + throw new UnsupportedOperationException( "不能修改系统保留基本数据类型的名称。" ); + } + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/ChargeType.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/ChargeType.java new file mode 100644 index 0000000..077d560 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/ChargeType.java @@ -0,0 +1,18 @@ +package com.ksa.model.bd; + +/** + * 费用类型模型 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class ChargeType extends BasicData { + + private static final long serialVersionUID = -219864488802023204L; + + @Override + public BasicDataType getType() { + return BasicDataType.CHARGE; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Currency.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Currency.java new file mode 100644 index 0000000..0cfd8a2 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Currency.java @@ -0,0 +1,61 @@ +package com.ksa.model.bd; + +import org.springframework.util.StringUtils; + +/** + * 货币数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class Currency extends BasicData { + + private static final long serialVersionUID = -3395552568108234099L; + + public static Currency RMB = new ReservedCurrency( "00-currency-RMB", "RMB", "人民币", 1.0f ); + public static Currency USD = new ReservedCurrency( "00-currency-USD", "USD", "美元", 6.8f ); + + public float getDefaultRate() { + if( !StringUtils.hasText( extra ) ) { + return 1.0f; + } + return Float.parseFloat( this.extra ); + } + + @Override + public BasicDataType getType() { + return BasicDataType.CURRENCY; + } + + /** + * 系统保留的默认货币类型,只读。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ + private static class ReservedCurrency extends Currency { + + private static final long serialVersionUID = -4404235952475840023L; + + ReservedCurrency( String id, String code, String name, float rate ) { + this.id = id; + this.code = code; + this.name = name; + this.extra = Float.toString( rate ); + } + + @Override + public void setId( String id ) { } + + @Override + public void setCode( String code ) { } + + @Override + public void setName( String name ) { } + + @Override + public void setExtra( String extra ) { } + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/CurrencyRate.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/CurrencyRate.java new file mode 100644 index 0000000..a3ed63f --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/CurrencyRate.java @@ -0,0 +1,59 @@ +package com.ksa.model.bd; + +import java.util.Date; + +import com.ksa.model.BaseModel; + +/** + * 货币汇率数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class CurrencyRate extends BaseModel { + + private static final long serialVersionUID = 8280004532527090297L; + + /** 汇率值 */ + protected float rate; + /** 汇率所属货币 */ + protected Currency currency = new Currency(); + /** 汇率所属日期 */ + protected Date month; + /** 汇率所属客户 */ + protected Partner partner = new Partner(); + + public Currency getCurrency() { + return currency; + } + + public void setCurrency( Currency currency ) { + this.currency = currency; + } + + public Date getMonth() { + return month; + } + + public void setMonth( Date month ) { + this.month = month; + } + + public Partner getPartner() { + return partner; + } + + public void setPartner( Partner partner ) { + this.partner = partner; + } + + public float getRate() { + return rate; + } + + public void setRate( float rate ) { + this.rate = rate; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Partner.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Partner.java new file mode 100644 index 0000000..2d38604 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/Partner.java @@ -0,0 +1,145 @@ +package com.ksa.model.bd; + +import com.ksa.model.BaseModel; +import com.ksa.model.security.User; + +/** + * 合作伙伴数据模型,是 客户( Customer )、供应商( Supplier )的基础模型。 + * + * 2012-11-26修改,将 Cusotmer 和 Supplier 模型合并为 Partner 统一管理。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class Partner extends BaseModel { + + private static final long serialVersionUID = -2263012417836719243L; + + /** 编码 */ + protected String code; + /** 名称 */ + protected String name; + /** 别名 */ + protected String alias; + /** 地址 */ + protected String address; + /** 付款周期,单位:天 (payment periods 简写为 pp) */ + protected int pp = 30; + /** 销售代表 */ + protected User saler; + /** 合作伙伴类型 */ + protected PartnerType[] types; + /** 备注 */ + protected String note; + /** 排序 */ + protected int rank; + /** 是否常用 */ + protected int important; + /** 提单中常用的附加信息 */ + protected String[] extras; + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress( String address ) { + this.address = address; + } + + public int getPp() { + return pp; + } + + public void setPp( int pp ) { + this.pp = pp; + } + + public User getSaler() { + if( saler == null ) { + saler = new User(); + } + return saler; + } + + public void setSaler( User saler ) { + this.saler = saler; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + + public String getAlias() { + return alias; + } + + + public void setAlias( String alias ) { + this.alias = alias; + } + + public PartnerType[] getTypes() { + return types; + } + + public void setTypes( PartnerType[] types ) { + this.types = types; + } + + public void setTypeIds( String[] ids ) { + if( ids != null && ids.length > 0 ) { + this.types = new PartnerType[ ids.length ]; + for( int i = 0; i < ids.length; i++ ) { + types[i] = new PartnerType(); + types[i].setId( ids[i] ); + } + } + } + + public String[] getExtras() { + return extras; + } + + public void setExtras( String[] extras ) { + this.extras = extras; + } + + public int getRank() { + return rank; + } + + public void setRank( int rank ) { + this.rank = rank; + } + + public int getImportant() { + return important; + } + + public void setImportant( int important ) { + this.important = important; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/PartnerType.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/PartnerType.java new file mode 100644 index 0000000..7a46082 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/bd/PartnerType.java @@ -0,0 +1,18 @@ +package com.ksa.model.bd; + +/** + * 合作伙伴类型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class PartnerType extends BasicData { + + private static final long serialVersionUID = -6742807466154803029L; + + @Override + public BasicDataType getType() { + return BasicDataType.DEPARTMENT; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/DebitNoteCharge.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/DebitNoteCharge.java new file mode 100644 index 0000000..c268afd --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/DebitNoteCharge.java @@ -0,0 +1,72 @@ +package com.ksa.model.business; + +import java.io.Serializable; + +import com.ksa.model.bd.Currency; +import com.ksa.model.finance.Charge; +import com.ksa.model.finance.FinanceModel; + +public class DebitNoteCharge extends FinanceModel implements Serializable { + + private static final long serialVersionUID = 2772543734062428868L; + + public DebitNoteCharge() {} + + public DebitNoteCharge( Charge charge ) { + this.item = charge.getType(); + this.unit = charge.getPrice() != null ? Float.toString( charge.getPrice() ) : ""; + this.note = charge.getNote(); + this.amount = charge.getAmount(); + this.currency = charge.getCurrency(); + this.direction = charge.getDirection(); + this.nature = charge.getNature(); + } + + protected String item; + protected String unit; + protected String note; + protected Float amount; + protected Currency currency; + + public String getItem() { + return item; + } + + public void setItem( String item ) { + this.item = item; + } + + public String getUnit() { + return unit; + } + + public void setUnit( String unit ) { + this.unit = unit; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + public Float getAmount() { + return amount; + } + + public void setAmount( Float amount ) { + this.amount = amount; + } + + public Currency getCurrency() { + return currency; + } + + public void setCurrency( Currency currency ) { + this.currency = currency; + } + + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillCharge.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillCharge.java new file mode 100644 index 0000000..fc91368 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillCharge.java @@ -0,0 +1,195 @@ +package com.ksa.model.business; + +import java.io.Serializable; + +import org.apache.commons.lang.StringUtils; + +import com.ksa.model.bd.Currency; + +/** + * 面单中使用的费用。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class RecordBillCharge implements Serializable, Comparable { + + private static final long serialVersionUID = -9073037770695093352L; + + /** 费用类型 */ + protected String type; + + /** 结算对象 */ + protected String customer; + + /** 结算货币 */ + protected Currency incomeCurrency; + + /** 单价 */ + protected Float income; + + /** 数量 */ + protected Float incomeAmount; + + /** 费用金额 = 单价 * 数量 */ + protected String incomeTotal; + + /** 结算货币 */ + protected Currency expenseCurrency; + + /** 单价 */ + protected Float expense; + + /** 数量 */ + protected Float expenseAmount; + + /** 费用金额 = 单价 * 数量 */ + protected String expenseTotal; + + /** 结算对象 */ + protected String agent; + + public String getType() { + return type; + } + + public void setType( String type ) { + this.type = type; + } + + public String getCustomer() { + return customer; + } + + public void setCustomer( String customer ) { + this.customer = customer; + } + + public Currency getIncomeCurrency() { + return incomeCurrency; + } + + public void setIncomeCurrency( Currency incomeCurrency ) { + this.incomeCurrency = incomeCurrency; + } + + public Float getIncome() { + return income; + } + + public void setIncome( Float income ) { + this.income = income; + } + + public Float getIncomeAmount() { + return incomeAmount; + } + + public void setIncomeAmount( Float incomeAmount ) { + this.incomeAmount = incomeAmount; + } + + public String getIncomeTotal() { + return incomeTotal; + } + + public void setIncomeTotal( String incomeTotal ) { + this.incomeTotal = incomeTotal; + } + + public Currency getExpenseCurrency() { + return expenseCurrency; + } + + public void setExpenseCurrency( Currency expenseCurrency ) { + this.expenseCurrency = expenseCurrency; + } + + public Float getExpense() { + return expense; + } + + public void setExpense( Float expense ) { + this.expense = expense; + } + + public Float getExpenseAmount() { + return expenseAmount; + } + + public void setExpenseAmount( Float expenseAmount ) { + this.expenseAmount = expenseAmount; + } + + public String getExpenseTotal() { + return expenseTotal; + } + + public void setExpenseTotal( String expenseTotal ) { + this.expenseTotal = expenseTotal; + } + + public String getAgent() { + return agent; + } + + public void setAgent( String agent ) { + this.agent = agent; + } + + @Override + public int compareTo( RecordBillCharge o ) { + // 按客户排列 + if( o == null ) { + return -1; + } + int result = compareString( this.customer, o.customer ); + + if( result == 0 ) { + // 客户相同就按币种继续排列 + result = compareCurrency( this.incomeCurrency, o.incomeCurrency ); + } + + if( result == 0 ) { + result = compareString( this.agent, o.agent ); + } + + if( result == 0 ) { + result = compareCurrency( this.expenseCurrency, o.expenseCurrency ); + } + + return result; + } + + private int compareString( String c1, String c2 ) { + return ( StringUtils.isEmpty( c2 ) ? "" : c2 ).compareTo( StringUtils.isEmpty( c1 ) ? "" : c1 ); + } + + private int compareCurrency( Currency c1, Currency c2 ) { + if( c1 == null ) { + return 1; + } + if( c2 == null ) { + return -1; + } + // 美元排前 + if( Currency.USD.getId().equals( c1.getId() ) ) { + return -1; + } + if( Currency.USD.getId().equals( c2.getId() ) ) { + return 1; + } + + // 人民币次之 + if( Currency.RMB.getId().equals( c1.getId() ) ) { + return -1; + } + if( Currency.RMB.getId().equals( c2.getId() ) ) { + return 1; + } + + return compareString( c1.getName(), c2.getName() ); + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillChargeGather.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillChargeGather.java new file mode 100644 index 0000000..66e711b --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillChargeGather.java @@ -0,0 +1,63 @@ +package com.ksa.model.business; + +import com.ksa.model.bd.Currency; + + +public class RecordBillChargeGather implements Comparable { + + protected Float income = 0f; + protected Float expense = 0f; + protected Float rate = 0f; + protected Currency currency; + + public RecordBillChargeGather() { } + + public RecordBillChargeGather addIncome( float value ) { + this.income += value; + return this; + } + + public RecordBillChargeGather addExpense( float value ) { + this.expense += value; + return this; + } + + public Float getIncome() { + return income; + } + + public void setIncome( Float income ) { + this.income = income; + } + + public Float getExpense() { + return expense; + } + + public void setExpense( Float expense ) { + this.expense = expense; + } + + public Float getRate() { + return rate; + } + + public void setRate( Float rate ) { + this.rate = rate; + } + + public Currency getCurrency() { + return currency; + } + + public void setCurrency( Currency currency ) { + this.currency = currency; + } + + @Override + public int compareTo( RecordBillChargeGather o ) { + return this.currency.getRank() - o.currency.getRank(); + } + + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillProfit.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillProfit.java new file mode 100644 index 0000000..a20729a --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/business/RecordBillProfit.java @@ -0,0 +1,77 @@ +package com.ksa.model.business; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + + +public class RecordBillProfit implements Serializable { + + private static final long serialVersionUID = 1736560648911429449L; + + protected Collection gathers; + + protected Float incomeRMB = 0f; + protected Float expenseRMB = 0f; + + public Float getIncomeRMB() { + return incomeRMB; + } + + public void setIncomeRMB( Float incomeRMB ) { + this.incomeRMB = incomeRMB; + } + + public int getGatherCount() { + if( gathers == null ) { + return 0; + } else { + return gathers.size(); + } + } + + public Float getIncome() { + Float income = incomeRMB; + if( gathers != null && gathers.size() > 0 ) { + for( RecordBillChargeGather gather : gathers ) { + income += ( gather.getIncome() * gather.getRate() ); + } + } + return income; + } + + public Float getExpense() { + Float expense = expenseRMB; + if( gathers != null && gathers.size() > 0 ) { + for( RecordBillChargeGather gather : gathers ) { + expense += ( gather.getExpense() * gather.getRate() ); + } + } + return expense; + } + + public Float getExpenseRMB() { + return expenseRMB; + } + + public void setExpenseRMB( Float expenseRMB ) { + this.expenseRMB = expenseRMB; + } + + public Collection getGathers() { + if( gathers == null ) { + return Collections.emptyList(); + } + return gathers; + } + + public void setGathers( Collection gathers ) { + List list = new ArrayList( gathers ); + Collections.sort( list ); + this.gathers = list; + } + + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Account.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Account.java new file mode 100644 index 0000000..99a6d1d --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Account.java @@ -0,0 +1,137 @@ +package com.ksa.model.finance; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import com.ksa.model.bd.Partner; +import com.ksa.model.security.User; + +/** + * 结账单数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class Account extends FinanceModel { + + private static final long serialVersionUID = 889478497102794302L; + + /** 结算单/对账单标识 */ + protected String code; + + /** 结算对象 */ + protected Partner target; + + /** 结账单所含全部费用 */ + protected List charges; + + /** 结账单所对应的所有销账发票/单据列表 */ + protected List invoices; + + /** 创建人 */ + protected User creator = new User(); + + /** 创建日期 */ + protected Date createdDate = new Date(); + + /** 付款截止日期 */ + protected Date deadline; + + /** 结清日期 */ + protected Date paymentDate; + + /** 结账单当前状态 */ + protected int state; + + /** 备注 */ + protected String note; + + public Partner getTarget() { + if( target == null ) { + target = new Partner(); + } + return target; + } + + public void setTarget( Partner target ) { + this.target = target; + } + + public List getCharges() { + return charges; + } + + public void setCharges( List charges ) { + this.charges = charges; + if( this.charges != null && this.charges.size() > 0 ) { + Collections.sort( this.charges ); + } + } + + public List getInvoices() { + return invoices; + } + + public void setInvoices( List invoices ) { + this.invoices = invoices; + } + + public User getCreator() { + return creator; + } + + public void setCreator( User creator ) { + this.creator = creator; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate( Date createdDate ) { + this.createdDate = createdDate; + } + + public Date getDeadline() { + return deadline; + } + + public void setDeadline( Date deadline ) { + this.deadline = deadline; + } + + public Date getPaymentDate() { + return paymentDate; + } + + public void setPaymentDate( Date paymentDate ) { + this.paymentDate = paymentDate; + } + + public int getState() { + return state; + } + + public void setState( int state ) { + this.state = state; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountCurrencyRate.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountCurrencyRate.java new file mode 100644 index 0000000..e0cc7c4 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountCurrencyRate.java @@ -0,0 +1,19 @@ +package com.ksa.model.finance; + +import com.ksa.model.bd.CurrencyRate; + +public class AccountCurrencyRate extends CurrencyRate { + + private static final long serialVersionUID = 7598185185140077626L; + + /** 所属结算单 */ + protected Account account = new Account(); + + public Account getAccount() { + return account; + } + + public void setAccount( Account account ) { + this.account = account; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountState.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountState.java new file mode 100644 index 0000000..7aa6928 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/AccountState.java @@ -0,0 +1,47 @@ +package com.ksa.model.finance; + +import com.ksa.model.ModelState; + + +public class AccountState extends ModelState { + + /** 处理中,对于收入款的结算单是开票阶段,对于支出款的对账单是支付阶段。*/ + public static final int PROCESSING = 0x1; + + /** 结算完毕 */ + public static final int SETTLED = 0x20; + + /** + * 判断结算单/对账单状态是否为 处理中。 + * @param state 待判断状态 + */ + public static boolean isProcessing( int state ) { + return ( PROCESSING & state ) > 0; + } + + /** + * 将结算单/对账单状态设置为 处理中。 + * @param state 待设置状态 + * @return 表示 处理中 的状态 + */ + public static int setProcessing( int state ) { + return state | PROCESSING; // 处理中 + } + + /** + * 判断结算单/对账单状态是否为 结算完毕。 + * @param state 待判断状态 + */ + public static boolean isSettled( int state ) { + return ( SETTLED & state ) > 0; + } + + /** + * 将结算单/对账单状态设置为 结算完毕。 + * @param state 待设置状态 + * @return 表示 结算完毕 的状态 + */ + public static int setSettled( int state ) { + return state | SETTLED; // 结算完毕 + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteCharge.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteCharge.java new file mode 100644 index 0000000..59bde44 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteCharge.java @@ -0,0 +1,21 @@ +package com.ksa.model.finance; + +import java.util.List; + +import com.ksa.model.logistics.BookingNote; + +public class BookingNoteCharge extends BookingNote { + + private static final long serialVersionUID = -1899021508699930132L; + + /** 结账单所含全部费用 */ + protected List charges; + + public List getCharges() { + return charges; + } + + public void setCharges( List charges ) { + this.charges = charges; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteChargeState.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteChargeState.java new file mode 100644 index 0000000..aae7327 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteChargeState.java @@ -0,0 +1,210 @@ +package com.ksa.model.finance; + +import com.ksa.model.logistics.BookingNoteState; + + +public class BookingNoteChargeState extends BookingNoteState { + + // 底位到高位数:1到4保持不动 + // 收入+国内(d:1,n:1):5到8 --> 移位 4 + // 支出+国内(d:-1,n:1):9到12 --> 移位 8 + // 收入+境外(d:1,n:-1):13到16 --> 移位 12 + // 支出+境外(d:-1,n:-1):17到20 --> 移位 16 + public static int computeShift( int direction, int nature ) { + int d = ( direction >= 0 ? 1 : -1 ); + int n = ( nature >= 0 ? 1 : -1 ); + return 10 - 2 * d - 4 * n; + } + + // 只区别境内和境外 + //境内:1到4 --> 移位0 + //境外:21到24 --> 移动20 + public static int computeShift( int nature ) { + // 大于等于0表示境内,小于0表示境外 + return ( nature >= 0 ? 0 : 20 ); + } + + /** 表示托单已经录入费用数据。*/ + public static final int ENTERING = 0x1; + + /** + * 判断托单费用状态是否为 已录入。 + * @param state 待判断状态 + */ + public static boolean isEntering( int state ) { + return ( ENTERING & state ) > 0; + } + /** + * 将当前托单费用状态设置为 已录入。 + * @param state 待设置状态 + * @return 表示 已录入 的状态 + */ + public static int setEntering( int state ) { + return state | ENTERING; // 录入中 + } + + /** + * 将当前托单费用状态设置为 未录入。 + * @param state 待设置状态 + * @return 表示 未录入 的状态 + */ + public static int setUnentering( int state ) { + return state & ~ENTERING; // 未录入 + } + + /** + * 判断托单费用状态是否为 已录入。 + * @param state 待判断状态 + */ + public static boolean isEntering( int state, int direction, int nature ) { + return ( ( ENTERING << computeShift( direction, nature ) ) & state ) > 0; + } + + /** + * 将当前托单费用状态设置为 已录入。 + * @param state 待设置状态 + * @return 表示 已录入 的状态 + */ + public static int setEntering( int state, int direction, int nature ) { + return state | ( ENTERING << computeShift( direction, nature ) ) ; // 录入中 + } + + /** + * 判断托单费用状态是否为 已录入。 + * @param state 待判断状态 + */ + public static boolean isEntering( int state, int nature ) { + return ( ( ENTERING << computeShift( nature ) ) & state ) > 0; + } + + /** + * 将当前托单费用状态设置为 未录入。 + * @param state 待设置状态 + * @return 表示 未录入 的状态 + */ + public static int setUnentering( int state, int direction, int nature ) { + return state & ~( ENTERING << computeShift( direction, nature ) ); // 未录入 + } + + /** + * 将当前托单费用状态设置为 已录入。 + * @param state 待设置状态 + * @return 表示 已录入 的状态 + */ + public static int setEntering( int state, int nature ) { + return state | ( ENTERING << computeShift( nature ) ) ; // 录入中 + } + + /** + * 将当前托单费用状态设置为 未录入。 + * @param state 待设置状态 + * @return 表示 未录入 的状态 + */ + public static int setUnentering( int state, int nature ) { + return state & ~( ENTERING << computeShift( nature ) ); // 未录入 + } + + /** + * 判断业务对象状态是否为 审核中。 + * @param state 待判断状态 + */ + public static boolean isChecking( int state, int direction, int nature ) { + return ( ( CHECKING << computeShift( direction, nature ) ) & state ) > 0; + } + + /** + * 将当前状态设置为 审核中。 + * @param state 待设置状态 + * @return 表示 审核中 的状态 + */ + public static int setChecking( int state, int direction, int nature ) { + return state | ( CHECKING << computeShift( direction, nature ) ); + } + + /** + * 将当前状态设置为 非审核中。 + * @param state 待设置状态 + * @return 表示 非审核中 的状态 + */ + public static int setUnchecking( int state, int direction, int nature ) { + return state & ~ ( CHECKING << computeShift( direction, nature ) ); + } + + /** + * 判断业务对象状态是否为 审核中。 + * @param state 待判断状态 + */ + public static boolean isChecking( int state, int nature ) { + return ( ( CHECKING << computeShift( nature ) ) & state ) > 0; + } + + /** + * 将当前状态设置为 审核中。 + * @param state 待设置状态 + * @return 表示 审核中 的状态 + */ + public static int setChecking( int state, int nature ) { + return state | ( CHECKING << computeShift( nature ) ); + } + + /** + * 将当前状态设置为 非审核中。 + * @param state 待设置状态 + * @return 表示 非审核中 的状态 + */ + public static int setUnchecking( int state, int nature ) { + return state & ~ ( CHECKING << computeShift( nature ) ); + } + + /** + * 判断业务对象状态是否为 审核通过。 + * @param state 待判断状态 + */ + public static boolean isChecked( int state, int direction, int nature ) { + return ( ( CHECKED << computeShift( direction, nature ) ) & state ) > 0; + } + + /** + * 将当前状态设置为 审核通过。 + * @param state 待设置状态 + * @return 表示 审核通过 的状态 + */ + public static int setChecked( int state, int direction, int nature ) { + return state | ( CHECKED << computeShift( direction, nature ) ); + } + + /** + * 将当前状态设置为 未审核通过。 + * @param state 待设置状态 + * @return 表示 未审核通过 的状态 + */ + public static int setUnchecked( int state, int direction, int nature ) { + return state & ~( CHECKED << computeShift( direction, nature ) ); + } + + /** + * 判断业务对象状态是否为 审核通过。 + * @param state 待判断状态 + */ + public static boolean isChecked( int state, int nature ) { + return ( ( CHECKED << computeShift( nature ) ) & state ) > 0; + } + + /** + * 将当前状态设置为 审核通过。 + * @param state 待设置状态 + * @return 表示 审核通过 的状态 + */ + public static int setChecked( int state, int nature ) { + return state | ( CHECKED << computeShift( nature ) ); + } + + /** + * 将当前状态设置为 未审核通过。 + * @param state 待设置状态 + * @return 表示 未审核通过 的状态 + */ + public static int setUnchecked( int state, int nature ) { + return state & ~( CHECKED << computeShift( nature ) ); + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteProfit.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteProfit.java new file mode 100644 index 0000000..99d889f --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/BookingNoteProfit.java @@ -0,0 +1,84 @@ +package com.ksa.model.finance; + +import java.util.Date; +import java.util.List; + +import com.ksa.model.bd.Partner; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.security.User; + +public class BookingNoteProfit extends BookingNote { + + private static final long serialVersionUID = -3114022945617180552L; + + /** 结账单所含全部费用 */ + protected List charges; + + public List getCharges() { + return charges; + } + + public void setCharges( List charges ) { + this.charges = charges; + } + + // ---------------- 隐藏不必要的属性 + + @Override + public Partner getNotify() { + return null; + } + + @Override + public Partner getShippingAgent() { + return null; + } + + @Override + public Partner getVehicleTeam() { + return null; + } + + public static class ProfitCharge extends Charge { + + // --------- 隐藏不必要的属性 + + private static final long serialVersionUID = -4086743282835712532L; + + public String getAccountId() { + return super.account.getId(); + } + + public void setAccountId( String accountId ) { + super.account.setId( accountId ); + } + + @Override + public Date getCreatedDate() { + return null; + } + + @Override + public Account getAccount() { + return null; + } + + @Override + public BookingNote getBookingNote() { + return null; + } + + @Override + public User getCreator() { + return null; + } + + @Override + public Partner getTarget() { + return null; + } + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Charge.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Charge.java new file mode 100644 index 0000000..b5d82fd --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Charge.java @@ -0,0 +1,214 @@ +package com.ksa.model.finance; + +import java.util.Date; + +import org.apache.commons.lang.StringUtils; + +import com.ksa.model.ModelUtils; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.Partner; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.security.User; + +/** + * 通用收支费用模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class Charge extends FinanceModel implements Comparable { + + private static final long serialVersionUID = 5354349025984427982L; + + /** 结算对象 */ + protected Partner target = new Partner(); + /** 费用类型 */ + protected String type; + /** 结算货币 */ + protected Currency currency = new Currency(); + /** 单价 */ + protected Float price; + /** 数量 */ + protected Float quantity; + /** 费用金额 = 单价 * 数量 */ + protected float amount = 0.0f; + /** 创建日期 */ + protected Date createdDate = new Date(); + /** 备注 */ + protected String note; + /** 创建人 */ + protected User creator = new User(); + /** 所属对账单 */ + protected Account account = new Account(); + /** 所属托单 */ + protected BookingNote bookingNote = new BookingNote(); + /** 费用录入的序号 */ + protected int rank = 0; + + /** 是否已经汇总入结算对账单 */ + public boolean isSettle() { + // 所属对账单对象 Account 是持久化对象 则表明本条费用已经开出结算单。 + return ModelUtils.isPersistentObject( account ); + } + + public Partner getTarget() { + return target; + } + + public void setTarget( Partner target ) { + this.target = target; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate( Date createdDate ) { + this.createdDate = createdDate; + } + + public String getType() { + return type; + } + + public void setType( String type ) { + this.type = type; + } + + public Currency getCurrency() { + return currency; + } + + public void setCurrency( Currency currency ) { + this.currency = currency; + } + + public float getAmount() { + return amount; + } + + public void setAmount( float amount ) { + this.amount = amount; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + public User getCreator() { + return creator; + } + + public void setCreator( User creator ) { + this.creator = creator; + } + + public int getAccountState() { + if( account == null ) { + return -1; + } else { + if( StringUtils.isEmpty( account.getId() ) ) { + return -1; + } + return account.getState(); + } + } + + public void setAccountState( Integer state ) { + if( state != null ) { + if( account == null ) { + account = new Account(); + } + account.setState( state.intValue() ); + } + } + + public Account getAccount() { + return account; + } + + public void setAccount( Account account ) { + this.account = account; + } + + public BookingNote getBookingNote() { + return bookingNote; + } + + public void setBookingNote( BookingNote bookingNote ) { + this.bookingNote = bookingNote; + } + + public Float getPrice() { + return price; + } + + public void setPrice( Float price ) { + this.price = price; + } + + public Float getQuantity() { + return quantity; + } + + public void setQuantity( Float quantity ) { + this.quantity = quantity; + } + + /** + * TODO 费用按托单展示的 treegrid 模型所需属性 + * + * @return + */ + public String get_parentId() { + if( bookingNote != null ) { + return bookingNote.getId(); + } else { + return ""; + } + } + /** + * TODO 费用按托单展示的 treegrid 模型所需属性 + * 所属业务托单的业务代码 + * + * @return + */ + public String getCode() { + return type; + } + + public int getRank() { + return rank; + } + + public void setRank(int rank) { + this.rank = rank; + } + + @Override + public int compareTo( Charge c ) { + int rtn = 0; + if( c.bookingNote != null && this.bookingNote != null ) { + rtn = c.bookingNote.getSerialNumber() - this.bookingNote.getSerialNumber(); + } + if( rtn != 0 ) { + return rtn; + } + + String n1 = this.target.getName(); + if( n1 == null ) { n1 = ""; } + String n2 = c.target.getName(); + if( n2 == null ) { n2 = ""; } + + rtn = n1.compareTo( n2 ); + if( rtn == 0 ) { + rtn = this.currency.getRank() - c.currency.getRank(); + } + return rtn; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/FinanceModel.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/FinanceModel.java new file mode 100644 index 0000000..60f408e --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/FinanceModel.java @@ -0,0 +1,84 @@ +package com.ksa.model.finance; + +import com.ksa.model.BaseModel; + +/** + * 财务相关业务数据模型抽象基类。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public abstract class FinanceModel extends BaseModel { + + private static final long serialVersionUID = -4120373048344140760L; + + /** 收入相关数据类型标识 */ + public static final int INCOME = 1; + + /** 支出相关数据类型标识 */ + public static final int EXPENSE = -1; + + /** 财务相关数据的种类为:国内 */ + public static final int NATIVE = 1; + + /** 财务相关数据的种类为:境外 */ + public static final int FOREIGN = -1; + + /** 表示数据的收支方向:1 表示与收入相关的业务数据,-1表示与支出相关的业务数据。 */ + protected int direction = INCOME; + + /** 表示数据的类型:1表示国内数据,-1表示境外数据。*/ + protected int nature = NATIVE; + + public int getNature() { + return nature; + } + + public void setNature( int nature ) { + if( nature >= 0 ) { + this.nature = NATIVE; + } else { + this.nature = FOREIGN; + } + } + + public int getDirection() { + return direction; + } + + public void setDirection( int direction ) { + if( direction >= 0 ) { + this.direction = INCOME; + } else { + this.direction = EXPENSE; + } + } + + public static final boolean isIncome( FinanceModel model ) { + return model.getDirection() == INCOME; + } + public static final boolean isIncome( int direction ) { + return direction == INCOME; + } + public static final boolean isExpense( FinanceModel model ) { + return model.getDirection() == EXPENSE; + } + public static final boolean isExpense( int direction ) { + return direction == EXPENSE; + } + + public static final boolean isNative( FinanceModel model ) { + return model.getNature() == NATIVE; + } + public static final boolean isNative( int nature ) { + return nature == NATIVE; + } + public static final boolean isForeign( FinanceModel model ) { + return model.getNature() == FOREIGN; + } + public static final boolean isForeign( int nature ) { + return nature == FOREIGN; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Invoice.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Invoice.java new file mode 100644 index 0000000..914629f --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/finance/Invoice.java @@ -0,0 +1,135 @@ +package com.ksa.model.finance; + +import java.util.Date; + +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.Partner; +import com.ksa.model.security.User; + +/** + * 发票/付款单据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class Invoice extends FinanceModel { + + private static final long serialVersionUID = -1154012608385382024L; + + /** 发票代码 */ + protected String code; + /** 发票号码 */ + protected String number; + /** 税号 */ + protected String taxNumber; + + /** 票据结算单位 */ + protected Partner target = new Partner(); + + /** 票据类型 */ + protected String type; + /** 结算货币 */ + protected Currency currency = new Currency(); + /** 费用金额 */ + protected float amount = 0.0f; + + /** 创建日期 */ + protected Date createdDate = new Date(); + /** 创建人 */ + protected User creator = new User(); + + /** 备注 */ + protected String note; + /** 所属对账单 */ + protected Account account = new Account(); + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + public String getNumber() { + return number; + } + + public void setNumber( String number ) { + this.number = number; + } + + public String getTaxNumber() { + return taxNumber; + } + + public void setTaxNumber( String taxNumber ) { + this.taxNumber = taxNumber; + } + + public String getType() { + return type; + } + + public void setType( String type ) { + this.type = type; + } + + public Currency getCurrency() { + return currency; + } + + public void setCurrency( Currency currency ) { + this.currency = currency; + } + + public float getAmount() { + return amount; + } + + public void setAmount( float amount ) { + this.amount = amount; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate( Date createdDate ) { + this.createdDate = createdDate; + } + + public User getCreator() { + return creator; + } + + public void setCreator( User creator ) { + this.creator = creator; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + public Account getAccount() { + return account; + } + + public void setAccount( Account account ) { + this.account = account; + } + + public Partner getTarget() { + return target; + } + + public void setTarget( Partner target ) { + this.target = target; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/ArrivalNote.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/ArrivalNote.java new file mode 100644 index 0000000..902a352 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/ArrivalNote.java @@ -0,0 +1,311 @@ +package com.ksa.model.logistics; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.ksa.util.StringUtils; + + +public class ArrivalNote extends BaseLogisticsModel { + + private static final long serialVersionUID = 2741955471843294574L; + + private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + + protected BookingNote bookingNote; + + protected String date; + protected String code; + + protected String consignee; + protected String shipper; + + protected String vessel; + protected String voyage; + protected String mawb; + protected String hawb; + protected String container; + protected String seal; + protected String eta; + + protected String cy; + protected String loadingPort; + protected String dischargePort; + protected String deliverPlace; + + protected String cargoMark; + protected String cargoWeight; + protected String cargoVolumn; + protected String cargoDescription; + protected String cargo; + protected String pkg; + protected String count; + + protected String freight; + protected String charge; + protected String rate; + + @Override + public void initialModel( BookingNote bookingNote ) { + this.bookingNote = bookingNote; + this.date = format.format( new Date() ); + + + this.consignee = bookingNote.getConsignee().getAlias(); + this.shipper = bookingNote.getShipper().getAlias(); + + this.vessel = bookingNote.getRouteName(); + this.voyage = bookingNote.getRouteCode(); + this.mawb = bookingNote.getMawb(); + this.hawb = bookingNote.getHawb(); + if( StringUtils.hasText( this.mawb ) ) { + this.code = this.mawb; + int length = this.code.length(); + if( length > 4 ) { + this.code = code.substring( length - 4, length ); + } + } + if( bookingNote.getDestinationDate() != null ) { + this.eta = format.format( bookingNote.getDestinationDate() ); + } + + // 装货港 + this.loadingPort = bookingNote.getLoadingPort(); + if( this.loadingPort == null ) { + this.loadingPort = bookingNote.getDeparturePort(); + } + this.dischargePort = bookingNote.getDischargePort(); + if( this.dischargePort == null ) { + this.dischargePort = bookingNote.getDestinationPort(); + } + + + this.cargoMark = bookingNote.getShippingMark(); + if( bookingNote.getVolumn() != null ) { + this.cargoVolumn = bookingNote.getVolumn().toString(); + } + if( bookingNote.getWeight() != null ) { + this.cargoWeight = bookingNote.getWeight().toString(); + } + + + } + + public String getDate() { + return date; + } + + public void setDate( String date ) { + this.date = date; + } + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + public String getConsignee() { + return consignee; + } + + public void setConsignee( String consignee ) { + this.consignee = consignee; + } + + public String getShipper() { + return shipper; + } + + public void setShipper( String shipper ) { + this.shipper = shipper; + } + + public String getVessel() { + return vessel; + } + + public void setVessel( String vessel ) { + this.vessel = vessel; + } + + public String getVoyage() { + return voyage; + } + + public void setVoyage( String voyage ) { + this.voyage = voyage; + } + + public String getMawb() { + return mawb; + } + + public void setMawb( String mawb ) { + this.mawb = mawb; + } + + public String getHawb() { + return hawb; + } + + public void setHawb( String hawb ) { + this.hawb = hawb; + } + + public String getContainer() { + return container; + } + + public void setContainer( String container ) { + this.container = container; + } + + public String getSeal() { + return seal; + } + + public void setSeal( String seal ) { + this.seal = seal; + } + + public String getEta() { + return eta; + } + + public void setEta( String eta ) { + this.eta = eta; + } + + public String getCy() { + return cy; + } + + public void setCy( String cy ) { + this.cy = cy; + } + + public String getLoadingPort() { + return loadingPort; + } + + public void setLoadingPort( String loadingPort ) { + this.loadingPort = loadingPort; + } + + public String getDischargePort() { + return dischargePort; + } + + public void setDischargePort( String dischargePort ) { + this.dischargePort = dischargePort; + } + + public String getDeliverPlace() { + return deliverPlace; + } + + public void setDeliverPlace( String deliverPlace ) { + this.deliverPlace = deliverPlace; + } + + public String getCargoMark() { + return cargoMark; + } + + public void setCargoMark( String cargoMark ) { + this.cargoMark = cargoMark; + } + + public String getCargoWeight() { + return cargoWeight; + } + + public void setCargoWeight( String cargoWeight ) { + this.cargoWeight = cargoWeight; + } + + public String getCargoVolumn() { + return cargoVolumn; + } + + public void setCargoVolumn( String cargoVolumn ) { + this.cargoVolumn = cargoVolumn; + } + + public String getCargoDescription() { + return cargoDescription; + } + + public void setCargoDescription( String cargoDescription ) { + this.cargoDescription = cargoDescription; + } + + public String getCargo() { + return cargo; + } + + public void setCargo( String cargo ) { + this.cargo = cargo; + } + + public String getPkg() { + return pkg; + } + + public void setPkg( String pkg ) { + this.pkg = pkg; + } + + public String getCount() { + return count; + } + + public void setCount( String count ) { + this.count = count; + } + + + public BookingNote getBookingNote() { + return bookingNote; + } + + public void setBookingNote( BookingNote bookingNote ) { + this.bookingNote = bookingNote; + } + + + public String getFreight() { + return freight; + } + + + public void setFreight( String freight ) { + this.freight = freight; + } + + + public String getCharge() { + return charge; + } + + + public void setCharge( String charge ) { + this.charge = charge; + } + + + public String getRate() { + return rate; + } + + + public void setRate( String rate ) { + this.rate = rate; + } + + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BaseLogisticsModel.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BaseLogisticsModel.java new file mode 100644 index 0000000..1a1af36 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BaseLogisticsModel.java @@ -0,0 +1,49 @@ +package com.ksa.model.logistics; + +import org.springframework.util.StringUtils; + +import com.ksa.model.BaseModel; +import com.ksa.model.bd.BasicData; + +/** + * 物流管理抽象基类模型,基于托单数据模型生成的各类业务数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class BaseLogisticsModel extends BaseModel { + + private static final long serialVersionUID = -2573872052916999869L; + + /** 模型所属托单。 */ + protected BookingNote bookingNote = new BookingNote(); + + /** + * 根据给定的托单数据,生成相应的业务初始化数据。 + * @param bookingnote 托单模型数据 + */ + public void initialModel( BookingNote bookingNote ) { + + } + + public BookingNote getBookingNote() { + return bookingNote; + } + + public void setBookingNote( BookingNote bookingNote ) { + this.bookingNote = bookingNote; + } + + public String getAlias( BasicData data ) { + if( data == null ) { + return ""; + } else { + String alias = data.getAlias(); + if( StringUtils.hasText( alias ) ) { + return alias; + } + return data.getCode(); + } + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BillOfLading.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BillOfLading.java new file mode 100644 index 0000000..96b60f8 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BillOfLading.java @@ -0,0 +1,321 @@ +package com.ksa.model.logistics; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.springframework.util.StringUtils; + +public class BillOfLading extends BaseLogisticsModel { + + private static final long serialVersionUID = 7274595599352874513L; + + /** 提单对象 */ + protected String to; + /** 发布时间 */ + protected String publishDate; + /** 发货人 */ + protected String shipper; + /** 收货人 */ + protected String consignee; + /** 通知人 */ + protected String notify; + /** 提单编号 */ + protected String code; + /** 发送方式: 电放 or 正本 */ + protected String deliverType; + /** 客户编号 */ + protected String customerCode; + /** 本公司编号 */ + protected String selfCode; + /** 发件人 */ + protected String creator; + /** 提单类型 */ + protected String billType; + /** 备注 */ + protected String note; + /** 海外代理 */ + protected String agent; + /** 船名航次 */ + protected String vesselVoyage = ""; + /** 装货港 */ + protected String loadingPort; + /** 卸货港 */ + protected String dischargePort; + /** 目的港 */ + protected String destinationPort; + /** 货物标记 */ + protected String cargoMark; + /** 货物数量 */ + protected String cargoQuantity; + /** 货物名称 */ + protected String cargoName; + /** 货物毛重 */ + protected String cargoWeight; + /** 货物体积 */ + protected String cargoVolumn; + /** 箱号封号 */ + protected String cargoDescription; + /** 货物英文描述 */ + protected String cargoQuantityDescription; + /** 付款方式 */ + protected String payMode = "FREIGHT PREPAID"; // FREIGHT PREPAID or FREIGHT COLLECT + + @Override + public void initialModel( BookingNote bookingNote ) { + this.bookingNote = bookingNote; + this.cargoDescription = bookingNote.getCargoDescription(); + this.cargoQuantityDescription = bookingNote.getQuantityDescription(); + this.cargoMark = bookingNote.getShippingMark(); + this.cargoName = bookingNote.getCargoName(); + if( bookingNote.getQuantity() != null ) { + this.cargoQuantity = bookingNote.getQuantity() + " "; + } + if( StringUtils.hasText( bookingNote.getUnit() ) ) { + this.cargoQuantity = this.cargoQuantity + bookingNote.getUnit(); + } + if( bookingNote.getVolumn() != null ) { + this.cargoVolumn = bookingNote.getVolumn().toString(); + } + if( bookingNote.getWeight() != null ) { + this.cargoWeight = bookingNote.getWeight().toString(); + } + this.code = ""; + if( StringUtils.hasText( bookingNote.getMawb() ) ) { + this.code += bookingNote.getMawb(); + } + if( StringUtils.hasText( bookingNote.getHawb() ) ) { + this.code += ( " " + bookingNote.getHawb() ); + } + this.consignee = bookingNote.getConsignee().getAlias(); + this.shipper = bookingNote.getShipper().getAlias(); + //this.notify = bookingNote.getNotify().getAlias(); + if( StringUtils.hasText( bookingNote.getConsignee().getId() ) && + bookingNote.getConsignee().getId().equals( bookingNote.getNotify().getId() ) ) { + this.notify = "SAME AS CONSIGNEE"; + } else { + this.notify = bookingNote.getNotify().getAlias(); + } + + //this.creator = bookingNote.getCreator().getName(); + this.to = bookingNote.getCustomer().getName(); + this.customerCode = bookingNote.getCustomer().getCode(); + this.destinationPort = bookingNote.getDestinationPort(); + this.dischargePort = bookingNote.getDischargePort(); + + // 装货港 + this.loadingPort = bookingNote.getDeparturePort(); + if( ! StringUtils.hasText( this.loadingPort )) { + this.loadingPort = bookingNote.getLoadingPort(); + } + + this.publishDate = new SimpleDateFormat("yyyy-MM-dd").format( new Date() ); + // 船名航次 + if( StringUtils.hasText( bookingNote.getRouteName() ) ) { + this.vesselVoyage = bookingNote.getRouteName() + " "; + } + if( StringUtils.hasText( bookingNote.getRouteCode() ) ) { + this.vesselVoyage += bookingNote.getRouteCode(); + } + } + + public String getCargoQuantityDescription() { + return cargoQuantityDescription; + } + + public void setCargoQuantityDescription( String cargoQuantityDescription ) { + this.cargoQuantityDescription = cargoQuantityDescription; + } + + public String getTo() { + return to; + } + + public void setTo( String to ) { + this.to = to; + } + + public String getPublishDate() { + return publishDate; + } + + public void setPublishDate( String publishDate ) { + this.publishDate = publishDate; + } + + public String getShipper() { + return shipper; + } + + public void setShipper( String shipper ) { + this.shipper = shipper; + } + + public String getConsignee() { + return consignee; + } + + public void setConsignee( String consignee ) { + this.consignee = consignee; + } + + public String getNotify() { + return notify; + } + + public void setNotify( String notify ) { + this.notify = notify; + } + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + public String getCustomerCode() { + return customerCode; + } + + public void setCustomerCode( String customerCode ) { + this.customerCode = customerCode; + } + + public String getSelfCode() { + return selfCode; + } + + public void setSelfCode( String selfCode ) { + this.selfCode = selfCode; + } + + public String getCreator() { + return creator; + } + + public void setCreator( String creator ) { + this.creator = creator; + } + + public String getBillType() { + return billType; + } + + public void setBillType( String type ) { + this.billType = type; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + public String getAgent() { + return agent; + } + + public void setAgent( String agent ) { + this.agent = agent; + } + + public String getVesselVoyage() { + return vesselVoyage; + } + + public void setVesselVoyage( String vesselVoyage ) { + this.vesselVoyage = vesselVoyage; + } + + public String getLoadingPort() { + return loadingPort; + } + + public void setLoadingPort( String loadingPort ) { + this.loadingPort = loadingPort; + } + + public String getDischargePort() { + return dischargePort; + } + + public void setDischargePort( String dischargePort ) { + this.dischargePort = dischargePort; + } + + public String getDestinationPort() { + return destinationPort; + } + + public void setDestinationPort( String destinationPort ) { + this.destinationPort = destinationPort; + } + + public String getCargoMark() { + return cargoMark; + } + + public void setCargoMark( String cargoMark ) { + this.cargoMark = cargoMark; + } + + public String getCargoQuantity() { + return cargoQuantity; + } + + public void setCargoQuantity( String cargoQuantity ) { + this.cargoQuantity = cargoQuantity; + } + + public String getCargoName() { + return cargoName; + } + + public void setCargoName( String cargoName ) { + this.cargoName = cargoName; + } + + public String getCargoWeight() { + return cargoWeight; + } + + public void setCargoWeight( String cargoWeight ) { + this.cargoWeight = cargoWeight; + } + + public String getCargoVolumn() { + return cargoVolumn; + } + + public void setCargoVolumn( String cargoVolumn ) { + this.cargoVolumn = cargoVolumn; + } + + public String getCargoDescription() { + return cargoDescription; + } + + public void setCargoDescription( String cargoDescription ) { + this.cargoDescription = cargoDescription; + } + + public String getDeliverType() { + return deliverType; + } + + public void setDeliverType( String deliverType ) { + this.deliverType = deliverType; + } + + public String getPayMode() { + return payMode; + } + + public void setPayMode( String payMode ) { + this.payMode = payMode; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNote.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNote.java new file mode 100644 index 0000000..0fbd403 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNote.java @@ -0,0 +1,801 @@ +package com.ksa.model.logistics; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang.StringUtils; + +import com.ksa.model.BaseModel; +import com.ksa.model.bd.Partner; +import com.ksa.model.security.User; + +/** + * 托单数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + * + * +ALTER TABLE ksa_logistics_bookingnote Add column HS_CODE varchar(200) AFTER `CARGO_NAME`; +ALTER TABLE ksa_logistics_bookingnote Add column KEY_CONTENT varchar(1000) AFTER `CARGO_NAME`; +ALTER TABLE ksa_logistics_bookingnote Add column CARGO_PRICE varchar(200) AFTER `CARGO_NAME`; +ALTER TABLE ksa_logistics_bookingnote Add column CARGO_NAME_ENG varchar(1000) AFTER `CARGO_NAME`; + */ +public class BookingNote extends BaseModel implements Comparable { + + private static final long serialVersionUID = 6565906853578546538L; + + // 分隔符为 "全角空格/全角空格" + private static final String EXPRESS_CODE_SPLITTER = " / "; + + /** 海运出口类型 托单 */ + public static final String TYPE_SEA_EXPORT = "SE"; + + /** 海运进口类型 托单 */ + public static final String TYPE_SEA_IMPORT = "SI"; + + /** 空运出口类型 托单 */ + public static final String TYPE_AIR_EXPORT = "AE"; + + /** 空运进口类型 托单 */ + public static final String TYPE_AIR_IMPORT = "AI"; + + /** 国内运输类型 托单*/ + public static final String TYPE_NATIVE = "LY"; + + /** 捆包业务类型 托单*/ + public static final String TYPE_KB = "KB"; + + /** 内航线类型 托单*/ + public static final String TYPE_RH = "RH"; + + /** 搬场业务类型 托单*/ + public static final String TYPE_BC = "BC"; + + /** 仓储业务类型 托单*/ + public static final String TYPE_CC = "CC"; + + /** 公铁联运类型 托单*/ + public static final String TYPE_TL = "TL"; + + /** 证件代办类型 托单*/ + public static final String TYPE_ZJ = "ZJ"; + + /** 子类型 FCL */ + public static final String TYPE_SUB_FCL = "FCL"; + + /** 子类型 LCL */ + public static final String TYPE_SUB_LCL = "LCL"; + + // -------------- 基本信息 -------------- // + /** 提单类型:海运/空运、出口/进口 */ + protected String type; + + /** 提单子类型:FCL / LCL */ + protected String subType; + + /** 流水号 */ + protected int serialNumber; + + /** 编号 */ + protected String code; + + /** 客户 */ + protected Partner customer = new Partner(); + + /** 发票编号 */ + protected String invoiceNumber; + + /** 接单日期 */ + protected Date createdDate = new Date(); + + /** 记账日期 */ + protected Date chargeDate; + + /** 销售人员 */ + protected User saler = new User(); + + /** 操作员 */ + protected User creator = new User(); + + /** 代理 */ + protected Partner agent = new Partner(); + + /** 承运人 */ + protected Partner carrier = new Partner(); + + /** 船代 */ + protected Partner shippingAgent = new Partner(); + + // -------------- 货物信息 -------------- // + /** 品名 */ + protected String cargoName; + + /** 英文品名 */ + protected String cargoNameEng; + + protected String hsCode; + + /** 关键要素 */ + protected String keyContent; + + /** 价格 */ + protected String cargoPrice; + + /** 备注 */ + protected String cargoNote; + + /** 箱号封号 */ + protected String cargoDescription; + + /** 货物的箱类箱型箱量描述 */ + protected String cargoContainer; + + /** 唛头 */ + protected String shippingMark; + + /** 体积 */ + protected Float volumn; + + /** 毛重 */ + protected Float weight; + + /** 数量 */ + protected Integer quantity; + + /** 数量单位 */ + protected String unit; + + /** 数量英文描述 */ + protected String quantityDescription; + + // -------------- 提单信息 -------------- // + /** 开票抬头 */ + protected String title; + + /** 主提单编号 */ + protected String mawb; + + /** 副提单编号 */ + protected String hawb; + + /** 发货人 */ + protected Partner shipper = new Partner(); + + /** 收货人 */ + protected Partner consignee = new Partner(); + + /** 通知人 */ + protected Partner notify = new Partner(); + + // -------------- 航线信息 -------------- // + /** 出发地 */ + protected String departure; + + /** 目的地 */ + protected String destination; + + /** 起运港 */ + protected String departurePort; + + /** 目的港 */ + protected String destinationPort; + + /** 装货港 (起运中转) */ + protected String loadingPort; + + /** 卸货港(到港中转) */ + protected String dischargePort; + + /** 出航日 */ + protected Date departureDate; + + /** 到港日 */ + protected Date destinationDate; + + /** 货物 接收/派送 日期 */ + protected Date deliverDate; + + /** 航线:海运航线 or 空运航线 */ + protected String route; + + /** 船名 or 航班 */ + protected String routeName; + + /** 航次 or 航班 */ + protected String routeCode; + + // -------------- 报关信息 -------------- // + /** 报关行:customs house broker */ + protected Partner customsBroker = new Partner(); + + /** 报关单号/核销单号 */ + protected String customsCode; + + /** 报关日 */ + protected Date customsDate; + + /** 退单号 */ + protected String returnCode; + + /** 退单日期 */ + protected Date returnDate; + + /** 退单寄送日期 */ + protected Date returnDate2; + + /** 税单号 */ + protected String taxCode; + + /** 收到税单日期 */ + protected Date taxDate1; + + /** 寄出税单日期 */ + protected Date taxDate2; + + /** 快递单号 */ + protected String expressCode; + + // -------------- 车队信息 -------------- // + /** 车型 */ + protected String vehicleType; + + /** 车牌号 */ + protected String vehicleNumber; + + /** 车队 */ + protected Partner vehicleTeam = new Partner(); + + /** 托单货物信息 */ + protected List cargos = new ArrayList(); + + /** 托单状态 */ + protected int state; + + public String getType() { + return type; + } + + public void setType( String type ) { + this.type = type; + } + + public void setSubType( String subType ) { + this.subType = subType; + } + + public String getSubType() { + return subType; + } + + public int getSerialNumber() { + return serialNumber; + } + + public void setSerialNumber( int serialNumber ) { + this.serialNumber = serialNumber; + } + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + public Partner getCustomer() { + if( customer == null ) { + customer = new Partner(); + } + return customer; + } + + public void setCustomer( Partner customer ) { + this.customer = customer; + } + + public String getInvoiceNumber() { + return invoiceNumber; + } + + public void setInvoiceNumber( String invoiceNumber ) { + this.invoiceNumber = invoiceNumber; + } + + public Date getCreatedDate() { + return createdDate; + } + + public void setCreatedDate( Date createdDate ) { + this.createdDate = createdDate; + } + + public Date getChargeDate() { + return chargeDate; + } + + public void setChargeDate( Date chargeDate ) { + this.chargeDate = chargeDate; + } + + public User getSaler() { + return saler; + } + + public void setSaler( User saler ) { + this.saler = saler; + } + + public User getCreator() { + return creator; + } + + public void setCreator( User creator ) { + this.creator = creator; + } + + public Partner getAgent() { + if( agent == null ) { + agent = new Partner(); + } + return agent; + } + + public void setAgent( Partner agent ) { + this.agent = agent; + } + + public Partner getCarrier() { + if( carrier == null ) { + carrier = new Partner(); + } + return carrier; + } + + public void setCarrier( Partner carrier ) { + this.carrier = carrier; + } + + public Partner getShippingAgent() { + if( shippingAgent == null ) { + shippingAgent = new Partner(); + } + return shippingAgent; + } + + public void setShippingAgent( Partner shippingAgent ) { + this.shippingAgent = shippingAgent; + } + + public String getCargoName() { + return cargoName; + } + + public void setCargoName( String cargoName ) { + this.cargoName = cargoName; + } + + public String getCargoNote() { + return cargoNote; + } + + public void setCargoNote( String cargoNote ) { + this.cargoNote = cargoNote; + } + + public String getCargoDescription() { + return cargoDescription; + } + + public void setCargoDescription( String cargoDescription ) { + this.cargoDescription = cargoDescription; + } + + public String getCargoContainer() { + return cargoContainer; + } + + public void setCargoContainer( String cargoContainer ) { + this.cargoContainer = cargoContainer; + } + + public String getShippingMark() { + return shippingMark; + } + + public void setShippingMark( String shippingMark ) { + this.shippingMark = shippingMark; + } + + public Float getVolumn() { + return volumn; + } + + public void setVolumn( Float volumn ) { + this.volumn = volumn; + } + + public Float getWeight() { + return weight; + } + + public void setWeight( Float weight ) { + this.weight = weight; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity( Integer quantity ) { + this.quantity = quantity; + } + + public String getUnit() { + return unit; + } + + public void setUnit( String unit ) { + this.unit = unit; + } + + public String getQuantityDescription() { + return quantityDescription; + } + + public void setQuantityDescription( String quantityDescription ) { + this.quantityDescription = quantityDescription; + } + + public String getTitle() { + return title; + } + + public void setTitle( String title ) { + this.title = title; + } + + public String getMawb() { + return mawb; + } + + public void setMawb( String mawb ) { + this.mawb = mawb; + } + + public String getHawb() { + return hawb; + } + + public void setHawb( String hawb ) { + this.hawb = hawb; + } + + public Partner getShipper() { + if( shipper == null ) { + shipper = new Partner(); + } + return shipper; + } + + public void setShipper( Partner shipper ) { + this.shipper = shipper; + } + + public Partner getConsignee() { + if( consignee == null ) { + consignee = new Partner(); + } + return consignee; + } + + public void setConsignee( Partner consignee ) { + this.consignee = consignee; + } + + public Partner getNotify() { + if( notify == null ) { + notify = new Partner(); + } + return notify; + } + + public void setNotify( Partner notify ) { + this.notify = notify; + } + + public String getDeparture() { + return departure; + } + + public void setDeparture( String departure ) { + this.departure = departure; + } + + public String getDestination() { + return destination; + } + + public void setDestination( String destination ) { + this.destination = destination; + } + + public String getDeparturePort() { + return departurePort; + } + + public void setDeparturePort( String departurePort ) { + this.departurePort = departurePort; + } + + public String getDestinationPort() { + return destinationPort; + } + + public void setDestinationPort( String destinationPort ) { + this.destinationPort = destinationPort; + } + + public String getLoadingPort() { + return loadingPort; + } + + public void setLoadingPort( String loadingPort ) { + this.loadingPort = loadingPort; + } + + public String getDischargePort() { + return dischargePort; + } + + public void setDischargePort( String dischargePort ) { + this.dischargePort = dischargePort; + } + + public Date getDepartureDate() { + return departureDate; + } + + public void setDepartureDate( Date departureDate ) { + this.departureDate = departureDate; + } + + public Date getDestinationDate() { + return destinationDate; + } + + public void setDestinationDate( Date destinationDate ) { + this.destinationDate = destinationDate; + } + + public Date getDeliverDate() { + return deliverDate; + } + + public void setDeliverDate( Date deliverDate ) { + this.deliverDate = deliverDate; + } + + public String getRoute() { + return route; + } + + public void setRoute( String route ) { + this.route = route; + } + + public String getRouteName() { + return routeName; + } + + public void setRouteName( String routeName ) { + this.routeName = routeName; + } + + public String getRouteCode() { + return routeCode; + } + + public void setRouteCode( String routeCode ) { + this.routeCode = routeCode; + } + + public Partner getCustomsBroker() { + if( customsBroker == null ) { + customsBroker = new Partner(); + } + return customsBroker; + } + + public void setCustomsBroker( Partner customsBroker ) { + this.customsBroker = customsBroker; + } + + public String getCustomsCode() { + return customsCode; + } + + public void setCustomsCode( String customsCode ) { + this.customsCode = customsCode; + } + + public Date getCustomsDate() { + return customsDate; + } + + public void setCustomsDate( Date customsDate ) { + this.customsDate = customsDate; + } + + public String getReturnCode() { + return returnCode; + } + + public void setReturnCode( String returnCode ) { + this.returnCode = returnCode; + } + + public Date getReturnDate() { + return returnDate; + } + + public void setReturnDate( Date returnDate ) { + this.returnDate = returnDate; + } + + public String getTaxCode() { + return taxCode; + } + + public void setTaxCode( String taxCode ) { + this.taxCode = taxCode; + } + + public Date getTaxDate1() { + return taxDate1; + } + + public void setTaxDate1( Date taxDate1 ) { + this.taxDate1 = taxDate1; + } + + public Date getTaxDate2() { + return taxDate2; + } + + public void setTaxDate2( Date taxDate2 ) { + this.taxDate2 = taxDate2; + } + + public String getTaxExpressCode() { + if( StringUtils.isEmpty( expressCode ) ) { + return ""; + } + String[] express = expressCode.split( EXPRESS_CODE_SPLITTER ); + if( express == null || express.length <= 0 ) { + return ""; + } else { + return express[0]; + } + } + + public void setTaxExpressCode( String tec ) { + StringBuilder sb = new StringBuilder( 64 ); + sb.append( tec == null ? "" : tec.trim() ).append( EXPRESS_CODE_SPLITTER ).append( getReturnExpressCode() ); + this.expressCode = sb.toString(); + } + + public String getReturnExpressCode() { + if( StringUtils.isEmpty( expressCode ) ) { + return ""; + } + String[] express = expressCode.split( EXPRESS_CODE_SPLITTER ); + if( express == null || express.length <= 1 ) { + return ""; + } else { + return express[1]; + } + } + + public void setReturnExpressCode( String tec ) { + StringBuilder sb = new StringBuilder( 64 ); + sb.append( getTaxExpressCode() ).append( EXPRESS_CODE_SPLITTER ).append( tec == null ? "" : tec.trim() ); + this.expressCode = sb.toString(); + } + + public String getExpressCode() { + return expressCode; + } + + public void setExpressCode( String expressCode ) { + this.expressCode = expressCode; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType( String vehicleType ) { + this.vehicleType = vehicleType; + } + + public String getVehicleNumber() { + return vehicleNumber; + } + + public void setVehicleNumber( String vehicleNumber ) { + this.vehicleNumber = vehicleNumber; + } + + public Partner getVehicleTeam() { + return vehicleTeam; + } + + public void setVehicleTeam( Partner vehicleTeam ) { + this.vehicleTeam = vehicleTeam; + } + + public List getCargos() { + return cargos; + } + + public void setCargos( List cargos ) { + this.cargos = cargos; + } + + public int getState() { + return state; + } + + public void setState( int state ) { + this.state = state; + } + + public Date getReturnDate2() { + return returnDate2; + } + + public void setReturnDate2( Date returnDate2 ) { + this.returnDate2 = returnDate2; + } + + public String getCargoNameEng() { + return cargoNameEng; + } + + public void setCargoNameEng(String cargoNameEng) { + this.cargoNameEng = cargoNameEng; + } + + public String getHsCode() { + return hsCode; + } + + public void setHsCode(String hsCode) { + this.hsCode = hsCode; + } + + public String getKeyContent() { + return keyContent; + } + + public void setKeyContent(String keyContent) { + this.keyContent = keyContent; + } + + public String getCargoPrice() { + return cargoPrice; + } + + public void setCargoPrice(String cargoPrice) { + this.cargoPrice = cargoPrice; + } + + @Override + public int compareTo( BookingNote o ) { + return this.serialNumber - o.serialNumber; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteCargo.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteCargo.java new file mode 100644 index 0000000..e48b57a --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteCargo.java @@ -0,0 +1,70 @@ +package com.ksa.model.logistics; + +import com.ksa.model.BaseModel; + +/** + * 提单中货物信息模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class BookingNoteCargo extends BaseModel { + + private static final long serialVersionUID = 1342906524991877423L; + + /** 名称 */ + protected String name; + + /** 箱类 */ + protected String category; + + /** 箱型 */ + protected String type; + + /** 数量 */ + protected int amount; + + /** 所属托单 */ + protected BookingNote bookingNote = new BookingNote(); + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } + + public String getCategory() { + return category; + } + + public void setCategory( String category ) { + this.category = category; + } + + public String getType() { + return type; + } + + public void setType( String type ) { + this.type = type; + } + + public int getAmount() { + return amount; + } + + public void setAmount( int amount ) { + this.amount = amount; + } + + public BookingNote getBookingNote() { + return bookingNote; + } + + public void setBookingNote( BookingNote bookingNote ) { + this.bookingNote = bookingNote; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteState.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteState.java new file mode 100644 index 0000000..521f881 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/BookingNoteState.java @@ -0,0 +1,57 @@ +package com.ksa.model.logistics; + +import com.ksa.model.ModelState; + + +public class BookingNoteState extends ModelState { + + /** 表示托单已经被删除。*/ + public static final int DELETED = -1; + + public static final int RETURNED = 0x10000000; // 十进制为 268435456 + + /*** + * 判断业务对象是否已经完成退单 + * @param state + * @return + */ + public static boolean isReturned( int state ) { + return ( RETURNED & state ) > 0; + } + + /*** + * 将当前业务对象的状态设置为 已退单。 + * @param state + * @return + */ + public static int setReturned( int state ) { + return state | RETURNED; + } + + /*** + * 将当前业务对象的状态设置为 未退单。 + * @param state + * @return + */ + public static int setUnreturned( int state ) { + return state & ~RETURNED ; + } + + + /** + * 判断托单对象状态是否为 已删除。 + * @param state 待判断状态 + */ + public static boolean isDeleted( int state ) { + return state == DELETED; + } + + /** + * 将当前托单状态设置为 已删除。 + * @param state 待设置状态 + * @return 表示 已删除 的状态 + */ + public static int setDeleted( int state ) { + return DELETED; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/Manifest.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/Manifest.java new file mode 100644 index 0000000..c1afdf9 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/Manifest.java @@ -0,0 +1,258 @@ +package com.ksa.model.logistics; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import org.springframework.util.StringUtils; + + +public class Manifest extends BaseLogisticsModel { + + private static final long serialVersionUID = 3056466545561955809L; + + /** 销售电话 */ + protected String salerTel = "88473507"; + /** 销售传真 */ + protected String salerFax = "88473508"; + /** 销售邮箱 */ + protected String salerEmail = "hangzhou@ksa.co.jp"; + + protected String saler; + protected String code; + protected String loadingPort; + protected String destinationPort; + protected String flightDate; // flight and date + protected String agent; + + protected String hawb; + protected String cargoWeight; + protected String cargoName; + protected String finalDestination; + protected String shipper; + protected String consignee; + protected String re; + + protected String totalHawb; + protected String totalPackages; + + @Override + public void initialModel( BookingNote bookingNote ) { + this.bookingNote = bookingNote; + + this.saler = bookingNote.getSaler().getName(); + // v3.4.2后 code 显示 mawb + //this.code = bookingNote.getCode(); + this.code = bookingNote.getMawb(); +// this.loadingPort = bookingNote.getLoadingPort(); +// this.destinationPort = bookingNote.getDestinationPort(); + this.loadingPort = bookingNote.getDeparture(); + if(!StringUtils.hasText( this.loadingPort )) { + this.loadingPort = bookingNote.getLoadingPort(); + } + this.destinationPort = bookingNote.getDestination(); + if(!StringUtils.hasText( this.destinationPort )) { + this.destinationPort = bookingNote.getDestinationPort(); + } + // v3.4.2后 agent 属性存放 consigned to 属性 + // this.agent = bookingNote.getAgent().getName(); + + StringBuilder sb = new StringBuilder(); + if( StringUtils.hasText( bookingNote.getRouteName() ) ) { + sb.append( bookingNote.getRouteName() ).append( " " ); + } + if( bookingNote.getDepartureDate() != null ) { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + sb.append( df.format( bookingNote.getDepartureDate() ) ); + } + this.flightDate = sb.toString(); + + this.hawb = bookingNote.getHawb(); + this.cargoWeight = bookingNote.getWeight() != null ? ( bookingNote.getWeight().toString() + " KGS" ) : ""; + // v3.4.2新增 + if( bookingNote.getQuantity() != null ) { + this.totalPackages = bookingNote.getQuantity() + " "; + } + if( StringUtils.hasText( bookingNote.getUnit() ) ) { + this.totalPackages = this.totalPackages + bookingNote.getUnit(); + } + this.cargoName = bookingNote.getCargoName(); + this.finalDestination = bookingNote.getDestination(); + this.shipper = bookingNote.getShipper().getAlias(); + this.consignee = bookingNote.getConsignee().getAlias(); + } + + + public String getSaler() { + return saler; + } + + + public void setSaler( String saler ) { + this.saler = saler; + } + + + public String getCode() { + return code; + } + + + public void setCode( String code ) { + this.code = code; + } + + + public String getLoadingPort() { + return loadingPort; + } + + + public void setLoadingPort( String loadingPort ) { + this.loadingPort = loadingPort; + } + + + public String getDestinationPort() { + return destinationPort; + } + + + public void setDestinationPort( String destinationPort ) { + this.destinationPort = destinationPort; + } + + + public String getFlightDate() { + return flightDate; + } + + + public void setFlightDate( String flightDate ) { + this.flightDate = flightDate; + } + + + public String getAgent() { + return agent; + } + + + public void setAgent( String agent ) { + this.agent = agent; + } + + + public String getHawb() { + return hawb; + } + + + public void setHawb( String hawb ) { + this.hawb = hawb; + } + + + public String getCargoWeight() { + return cargoWeight; + } + + + public void setCargoWeight( String cargoWeight ) { + this.cargoWeight = cargoWeight; + } + + + public String getCargoName() { + return cargoName; + } + + + public void setCargoName( String cargoName ) { + this.cargoName = cargoName; + } + + + public String getFinalDestination() { + return finalDestination; + } + + + public void setFinalDestination( String finalDestination ) { + this.finalDestination = finalDestination; + } + + + public String getShipper() { + return shipper; + } + + + public void setShipper( String shipper ) { + this.shipper = shipper; + } + + + public String getConsignee() { + return consignee; + } + + + public void setConsignee( String consignee ) { + this.consignee = consignee; + } + + + public String getRe() { + return re; + } + + + public void setRe( String re ) { + this.re = re; + } + + + public String getTotalHawb() { + return totalHawb; + } + + + public void setTotalHawb( String totalHawb ) { + this.totalHawb = totalHawb; + } + + + public String getTotalPackages() { + return totalPackages; + } + + + public void setTotalPackages( String totalPackages ) { + this.totalPackages = totalPackages; + } + + public String getSalerTel() { + return salerTel; + } + + public void setSalerTel( String salerTel ) { + this.salerTel = salerTel; + } + + public String getSalerFax() { + return salerFax; + } + + public void setSalerFax( String salerFax ) { + this.salerFax = salerFax; + } + + public String getSalerEmail() { + return salerEmail; + } + + public void setSalerEmail( String salerEmail ) { + this.salerEmail = salerEmail; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseBooking.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseBooking.java new file mode 100644 index 0000000..bcc0f33 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseBooking.java @@ -0,0 +1,354 @@ +package com.ksa.model.logistics; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.ksa.util.StringUtils; + +/** + * 订仓通知单数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class WarehouseBooking extends BaseLogisticsModel { + + private static final long serialVersionUID = 65019373225306151L; + + /** 销售电话 */ + protected String salerTel = "88473507"; + /** 销售传真 */ + protected String salerFax = "88473508"; + /** 销售邮箱 */ + protected String salerEmail = "hangzhou@ksa.co.jp"; + + /** 销售担当 */ + protected String saler; + + /** 销售担当 */ + protected String shipper; + + /** 销售担当 */ + protected String consignee; + + /** 销售担当 */ + protected String notify; + + /** 委托编号 */ + protected String code; + + /** 委托时间 */ + protected String createdDate; + + /** 起运港 */ + protected String departurePort; + + /** 目的港 */ + protected String destinationPort; + + /** 转船 */ + protected String switchShip; + + /** 分批 */ + protected String grouping; + + /** 运输方式 */ + protected String transportMode; + + /** 付款方式 */ + protected String paymentMode; + + /** 运费 */ + protected String freightCharge; + + /** 箱量 */ + protected String cargoContainer; + + /** 唛头 */ + protected String shippingMark; + + /** 品名 */ + protected String cargoName; + + /** 货物毛重 */ + protected String cargoWeight; + + /** 货物体积 */ + protected String cargoVolumn; + + /** 货物数量 */ + protected String cargoQuantity; + + /** 货物毛重 */ + protected String totalWeight; + + /** 货物体积 */ + protected String totalVolumn; + + /** 货物数量 */ + protected String totalQuantity; + + /** 注意事项 */ + protected String note; + + @Override + public void initialModel( BookingNote bookingNote ) { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + this.bookingNote = bookingNote; + + this.saler = bookingNote.getSaler().getName(); + this.consignee = bookingNote.getConsignee().getAlias(); + this.shipper = bookingNote.getShipper().getAlias(); + //this.notify = bookingNote.getNotify().getAlias(); + if( StringUtils.hasText( bookingNote.getConsignee().getId() ) && + bookingNote.getConsignee().getId().equals( bookingNote.getNotify().getId() ) ) { + this.notify = "SAME AS CONSIGNEE"; + } else { + this.notify = bookingNote.getNotify().getAlias(); + } + + this.createdDate = dateFormat.format( new Date() ); + this.code = bookingNote.getCode(); + + this.departurePort = bookingNote.getDeparturePort(); + this.destinationPort = bookingNote.getDestinationPort(); + + this.transportMode = bookingNote.getType().startsWith( "A" ) ? "空运" : "海运"; + + this.cargoContainer = bookingNote.getCargoContainer(); + if( !StringUtils.hasText( this.cargoContainer ) && bookingNote.cargos != null && bookingNote.cargos.size() > 0 ) { + StringBuilder sb = new StringBuilder(); + for( BookingNoteCargo cargo : bookingNote.cargos ) { + sb.append( " + ").append( cargo.getCategory() ).append( cargo.getType() ).append( "*" ).append( cargo.amount ); + } + this.cargoContainer = sb.substring( 3 ); + } + this.shippingMark = bookingNote.getShippingMark(); + this.cargoName = bookingNote.getCargoName(); + if( bookingNote.getQuantity() != null ) { + this.cargoQuantity = bookingNote.getQuantity().toString(); + if( bookingNote.getUnit() != null ) { + this.cargoQuantity += ( " " + bookingNote.getUnit() ); + } + this.totalQuantity = this.cargoQuantity; + } + if( bookingNote.getWeight() != null ) { + this.cargoWeight = bookingNote.getWeight().toString() + " KGS"; + this.totalWeight = this.cargoWeight; + } + if( bookingNote.getVolumn() != null ) { + this.cargoVolumn = bookingNote.getVolumn().toString() + " CBM"; + this.totalVolumn = this.cargoVolumn; + } + } + + public String getSaler() { + return saler; + } + + public void setSaler( String saler ) { + this.saler = saler; + } + + public String getShipper() { + return shipper; + } + + public void setShipper( String shipper ) { + this.shipper = shipper; + } + + public String getConsignee() { + return consignee; + } + + public void setConsignee( String consignee ) { + this.consignee = consignee; + } + + public String getNotify() { + return notify; + } + + public void setNotify( String notify ) { + this.notify = notify; + } + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + public String getCreatedDate() { + return createdDate; + } + + public void setCreatedDate( String createdDate ) { + this.createdDate = createdDate; + } + + public String getDeparturePort() { + return departurePort; + } + + public void setDeparturePort( String departurePort ) { + this.departurePort = departurePort; + } + + public String getDestinationPort() { + return destinationPort; + } + + public void setDestinationPort( String destinationPort ) { + this.destinationPort = destinationPort; + } + + public String getSwitchShip() { + return switchShip; + } + + public void setSwitchShip( String switchShip ) { + this.switchShip = switchShip; + } + + public String getGrouping() { + return grouping; + } + + public void setGrouping( String grouping ) { + this.grouping = grouping; + } + + public String getTransportMode() { + return transportMode; + } + + public void setTransportMode( String transportMode ) { + this.transportMode = transportMode; + } + + public String getPaymentMode() { + return paymentMode; + } + + public void setPaymentMode( String paymentMode ) { + this.paymentMode = paymentMode; + } + + public String getFreightCharge() { + return freightCharge; + } + + public void setFreightCharge( String freightCharge ) { + this.freightCharge = freightCharge; + } + + public String getCargoContainer() { + return cargoContainer; + } + + public void setCargoContainer( String cargoContainer ) { + this.cargoContainer = cargoContainer; + } + + public String getShippingMark() { + return shippingMark; + } + + public void setShippingMark( String shippingMark ) { + this.shippingMark = shippingMark; + } + + public String getCargoName() { + return cargoName; + } + + public void setCargoName( String cargoName ) { + this.cargoName = cargoName; + } + + public String getCargoWeight() { + return cargoWeight; + } + + public void setCargoWeight( String cargoWeight ) { + this.cargoWeight = cargoWeight; + } + + public String getCargoVolumn() { + return cargoVolumn; + } + + public void setCargoVolumn( String cargoVolumn ) { + this.cargoVolumn = cargoVolumn; + } + + public String getCargoQuantity() { + return cargoQuantity; + } + + public void setCargoQuantity( String cargoQuantity ) { + this.cargoQuantity = cargoQuantity; + } + + public String getTotalWeight() { + return totalWeight; + } + + public void setTotalWeight( String totalWeight ) { + this.totalWeight = totalWeight; + } + + public String getTotalVolumn() { + return totalVolumn; + } + + public void setTotalVolumn( String totalVolumn ) { + this.totalVolumn = totalVolumn; + } + + public String getTotalQuantity() { + return totalQuantity; + } + + public void setTotalQuantity( String totalQuantity ) { + this.totalQuantity = totalQuantity; + } + + public String getNote() { + return note; + } + + public void setNote( String note ) { + this.note = note; + } + + public String getSalerTel() { + return salerTel; + } + + public void setSalerTel( String salerTel ) { + this.salerTel = salerTel; + } + + public String getSalerFax() { + return salerFax; + } + + public void setSalerFax( String salerFax ) { + this.salerFax = salerFax; + } + + public String getSalerEmail() { + return salerEmail; + } + + public void setSalerEmail( String salerEmail ) { + this.salerEmail = salerEmail; + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseNoting.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseNoting.java new file mode 100644 index 0000000..e395b52 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/logistics/WarehouseNoting.java @@ -0,0 +1,342 @@ +package com.ksa.model.logistics; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 进仓通知单数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class WarehouseNoting extends BaseLogisticsModel { + + private static final long serialVersionUID = -3414203243419839677L; + + /** 销售电话 */ + protected String salerTel = "88473507"; + /** 销售传真 */ + protected String salerFax = "88473508"; + /** 销售邮箱 */ + protected String salerEmail = "hangzhou@ksa.co.jp"; + + /** 销售担当 */ + protected String saler; + /** TO */ + protected String to; + /** 进仓编号 */ + protected String code; + /** 进仓时间 */ + protected String createdDate; + /** 品名 */ + protected String cargoName; + /** 货物毛重 */ + protected String cargoWeight; + /** 货物体积 */ + protected String cargoVolumn; + /** 货物数量 */ + protected String cargoQuantity; + /** 委托客户 */ + protected String customer; + /** 起运港 */ + protected String loadingPort; + /** 卸货港 */ + protected String dischargePort; + /** 船名航次 */ + protected String vesselVoyage = ""; + /** 目的地 */ + protected String destination; + /** 出航日 */ + protected String departureDate; + /** 提单号 */ + protected String mawb; + /** 最晚入仓时间 */ + protected String entryDate; + /** 通知时间 */ + protected String informDate; + + /** 进仓地址 */ + protected String address; + /** 联系人 */ + protected String contact; + /** 电话 */ + protected String telephone; + /** 传真 */ + protected String fax; + + @Override + public void initialModel( BookingNote bookingNote ) { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + this.bookingNote = bookingNote; + + this.createdDate = dateFormat.format( new Date() ); + this.saler = bookingNote.getSaler().getId(); + this.code = bookingNote.getCode(); + this.cargoName = bookingNote.getCargoName(); + if( bookingNote.getQuantity() != null ) { + this.cargoQuantity = bookingNote.getQuantity().toString(); + if( bookingNote.getUnit() != null ) { + this.cargoQuantity += ( " " + bookingNote.getUnit() ); + } + } + + if( bookingNote.getWeight() != null ) { + this.cargoWeight = bookingNote.getWeight().toString() + " KGS"; + } + + if( bookingNote.getVolumn() != null ) { + this.cargoVolumn = bookingNote.getVolumn().toString() + " CBM"; + } + + this.customer = bookingNote.getCustomer().getId(); + this.loadingPort = bookingNote.getLoadingPort(); + this.dischargePort = bookingNote.getDischargePort(); + this.destination = bookingNote.getDestination(); + if( bookingNote.getDepartureDate() != null ) { + this.departureDate = dateFormat.format( bookingNote.getDepartureDate() ); + } + this.mawb = bookingNote.getMawb(); + + if( bookingNote.getRouteName() != null ) { + this.vesselVoyage = bookingNote.getRouteName() + " "; + } + if( bookingNote.getRouteCode() != null ) { + this.vesselVoyage += bookingNote.getRouteCode(); + } + } + + + public String getSaler() { + return saler; + } + + + public void setSaler( String saler ) { + this.saler = saler; + } + + + public String getTo() { + return to; + } + + + public void setTo( String to ) { + this.to = to; + } + + + public String getCode() { + return code; + } + + + public void setCode( String code ) { + this.code = code; + } + + + public String getCreatedDate() { + return createdDate; + } + + + public void setCreatedDate( String createdDate ) { + this.createdDate = createdDate; + } + + + public String getCargoName() { + return cargoName; + } + + + public void setCargoName( String cargoName ) { + this.cargoName = cargoName; + } + + + public String getCargoWeight() { + return cargoWeight; + } + + + public void setCargoWeight( String cargoWeight ) { + this.cargoWeight = cargoWeight; + } + + + public String getCargoVolumn() { + return cargoVolumn; + } + + + public void setCargoVolumn( String cargoVolumn ) { + this.cargoVolumn = cargoVolumn; + } + + + public String getCargoQuantity() { + return cargoQuantity; + } + + + public void setCargoQuantity( String cargoQuantity ) { + this.cargoQuantity = cargoQuantity; + } + + + public String getCustomer() { + return customer; + } + + + public void setCustomer( String customer ) { + this.customer = customer; + } + + + public String getLoadingPort() { + return loadingPort; + } + + + public void setLoadingPort( String loadingPort ) { + this.loadingPort = loadingPort; + } + + + public String getDischargePort() { + return dischargePort; + } + + + public void setDischargePort( String dischargePort ) { + this.dischargePort = dischargePort; + } + + + public String getVesselVoyage() { + return vesselVoyage; + } + + + public void setVesselVoyage( String vesselVoyage ) { + this.vesselVoyage = vesselVoyage; + } + + + public String getDestination() { + return destination; + } + + + public void setDestination( String destination ) { + this.destination = destination; + } + + + public String getDepartureDate() { + return departureDate; + } + + + public void setDepartureDate( String departureDate ) { + this.departureDate = departureDate; + } + + + public String getMawb() { + return mawb; + } + + + public void setMawb( String mawb ) { + this.mawb = mawb; + } + + + public String getEntryDate() { + return entryDate; + } + + + public void setEntryDate( String entryDate ) { + this.entryDate = entryDate; + } + + + public String getInformDate() { + return informDate; + } + + + public void setInformDate( String informDate ) { + this.informDate = informDate; + } + + + public String getAddress() { + return address; + } + + + public void setAddress( String address ) { + this.address = address; + } + + + public String getContact() { + return contact; + } + + + public void setContact( String contact ) { + this.contact = contact; + } + + + public String getTelephone() { + return telephone; + } + + + public void setTelephone( String telephone ) { + this.telephone = telephone; + } + + public String getFax() { + return fax; + } + + public void setFax( String fax ) { + this.fax = fax; + } + + public String getSalerTel() { + return salerTel; + } + + public void setSalerTel( String salerTel ) { + this.salerTel = salerTel; + } + + public String getSalerFax() { + return salerFax; + } + + public void setSalerFax( String salerFax ) { + this.salerFax = salerFax; + } + + public String getSalerEmail() { + return salerEmail; + } + + public void setSalerEmail( String salerEmail ) { + this.salerEmail = salerEmail; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Permission.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Permission.java new file mode 100644 index 0000000..21c50db --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Permission.java @@ -0,0 +1,39 @@ +package com.ksa.model.security; + +import java.io.Serializable; + +import com.ksa.model.BaseModel; + +/** + * KSA 系统的权限信息。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class Permission extends BaseModel implements Serializable { + + private static final long serialVersionUID = 2108049421789476504L; + + /** 权限名称 */ + protected String name; + /** 权限说明 */ + protected String description; + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription( String description ) { + this.description = description; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Role.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Role.java new file mode 100644 index 0000000..d9a596b --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/Role.java @@ -0,0 +1,50 @@ +package com.ksa.model.security; + +import java.io.Serializable; + +import com.ksa.model.BaseModel; + +/** + * KSA 系统的用户角色。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class Role extends BaseModel implements Serializable { + + private static final long serialVersionUID = -6531662775846984570L; + + /** 角色名称 */ + protected String name; + /** 角色说明 */ + protected String description; + /** 角色权限 */ + protected Permission[] permissions; + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription( String description ) { + this.description = description; + } + + public Permission[] getPermissions() { + return permissions; + } + + public void setPermissions( Permission[] permissions ) { + this.permissions = permissions; + } + + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/User.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/User.java new file mode 100644 index 0000000..0e7c896 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/model/security/User.java @@ -0,0 +1,84 @@ +package com.ksa.model.security; + +import java.io.Serializable; +import java.util.List; + +import com.ksa.model.BaseModel; + +/** + * KSA 系统的用户。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class User extends BaseModel implements Serializable { + + private static final long serialVersionUID = 715060664977488527L; + + /** 用户是否为锁定状态 */ + protected boolean locked = false; + /** 用户姓名 */ + protected String name; + /** 用户登录密码 */ + protected transient String password; + /** 用户邮箱 */ + protected String email; + /** 用户电话 */ + protected String telephone; + /** 用户所属角色 */ + protected List roles; + + public boolean isLocked() { + return locked; + } + + public boolean getLocked() { + return locked; + } + + public void setLocked( boolean locked ) { + this.locked = locked; + } + + public String getName() { + return name; + } + + public void setName( String name ) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword( String password ) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail( String email ) { + this.email = email; + } + + public String getTelephone() { + return telephone; + } + + public void setTelephone( String telephone ) { + this.telephone = telephone; + } + + public List getRoles() { + return roles; + } + + public void setRoles( List roles ) { + this.roles = roles; + } + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/BasicDataService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/BasicDataService.java new file mode 100644 index 0000000..577811d --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/BasicDataService.java @@ -0,0 +1,44 @@ +package com.ksa.service.bd; + +import com.ksa.model.bd.BasicData; + +/** + * 基础数据管理服务接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface BasicDataService { + /** + * 根据基础数据标识获取基础数据。 + * @param dataId 用户标识 + * @return 对应标识的基础数据 + * @throws RuntimeException 对应标识的用户不存在 + */ + BasicData loadBasicDataById( String dataId ) throws RuntimeException; + + /** + * 创建新基础数据。 + * @param data 将要被创建的基础数据 + * @return 创建成功后的基础数据 + * @throws RuntimeException + */ + BasicData createBasicData( BasicData data ) throws RuntimeException; + + /** + * 更新基础数据。 + * @param data 新的基础数据 + * @return 更新成功后的基础数据 + * @throws RuntimeException + */ + BasicData modifyBasicData( BasicData data ) throws RuntimeException; + + /** + * 移除基础数据。 + * @param data 需要被移除的基础数据 + * @return 移除成功后的基础数据 + * @throws RuntimeException + */ + BasicData removeBasicData( BasicData data ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/CurrencyRateService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/CurrencyRateService.java new file mode 100644 index 0000000..e3cff21 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/CurrencyRateService.java @@ -0,0 +1,74 @@ +package com.ksa.service.bd; + +import java.util.Date; +import java.util.List; + +import com.ksa.model.bd.CurrencyRate; + + +public interface CurrencyRateService { + + /** + * 保存结算货币汇率。 + * @param rate 结算货币汇率 + * @return 创建成功后的汇率 + * @throws RuntimeException + */ + CurrencyRate saveCurrencyRate( CurrencyRate rate ) throws RuntimeException; + + /** + * 获取当前系统已有货币的最新汇率。 + * @return 已有货币的最新汇率 + * @throws RuntimeException + */ + List loadLatestCurrencyRates() throws RuntimeException; + + /** + * 获取当前系统所有的汇率记录。 + * @return 当前系统所有的汇率记录 + * @throws RuntimeException + */ + List loadAllCurrencyRates() throws RuntimeException; + + /** + * 获取给定时间(包含)之前设定的最近汇率。 + * @param date 给定的时间界限 + * @return 给定时间(包含)之前的最近汇率 + * @throws RuntimeException + */ + List loadLatestCurrencyRates( Date date ) throws RuntimeException; + + /** + * 获取具体货币的最新汇率 + * @param currencyId 具体货币的标识 + * @return 具体货币的最新汇率 + * @throws RuntimeException + */ + CurrencyRate loadLatestCurrencyRate( String currencyId ) throws RuntimeException; + + /** + * 获取具体货币给定时间(包含)之前设定的最近汇率 + * @param currencyId 具体货币的标识 + * @param date 给定的时间界限 + * @return 具体货币的最新汇率 + * @throws RuntimeException + */ + CurrencyRate loadLatestCurrencyRate( String currencyId, Date date ) throws RuntimeException; + + /** + * 获取特定客户的汇率。 + * @param customerId 特定客户的标识 + * @return 特定客户的全部汇率 + * @throws RuntimeException + */ + List loadPartnerCurrencyRates( String customerId ) throws RuntimeException; + + /** + * 获取特定客户具体货币的汇率。 + * @param customerId 特定客户的标识 + * @param currencyId 具体货币的标识 + * @return 特定客户的汇率 + * @throws RuntimeException + */ + CurrencyRate loadPartnerCurrencyRate( String customerId, String currencyId ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/PartnerService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/PartnerService.java new file mode 100644 index 0000000..2dc9880 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/bd/PartnerService.java @@ -0,0 +1,53 @@ +package com.ksa.service.bd; + +import com.ksa.model.bd.Partner; + +/** + * 合作伙伴管理服务接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface PartnerService { + + /** + * 根据合作伙伴数据标识获取合作伙伴数据。 + * @param id 合作伙伴标识 + * @return 对应标识的合作伙伴数据 + * @throws RuntimeException 对应标识的合作伙伴不存在 + */ + Partner loadPartnerById( String id ) throws RuntimeException; + + /** + * 根据合作伙伴数据代码获取合作伙伴数据。 + * @param code 合作伙伴代码 + * @return 对应代码的合作伙伴数据 + * @throws RuntimeException 对应代码的合作伙伴不存在 + */ + Partner loadPartnerByCode( String code ) throws RuntimeException; + + /** + * 创建新合作伙伴数据。 + * @param partner 将要被创建的合作伙伴数据 + * @return 创建成功后的合作伙伴数据 + * @throws RuntimeException + */ + Partner createPartner( Partner partner ) throws RuntimeException; + + /** + * 更新合作伙伴数据。 + * @param partner 新的合作伙伴数据 + * @return 更新成功后的合作伙伴数据 + * @throws RuntimeException + */ + Partner modifyPartner( Partner partner ) throws RuntimeException; + + /** + * 移除合作伙伴数据。 + * @param partner 需要被移除的合作伙伴数据 + * @return 移除成功后的合作伙伴数据 + * @throws RuntimeException + */ + Partner removePartner( Partner partner ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/AccountService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/AccountService.java new file mode 100644 index 0000000..8fd4aa8 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/AccountService.java @@ -0,0 +1,62 @@ +package com.ksa.service.finance; + +import java.util.List; + +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.AccountCurrencyRate; +import com.ksa.model.logistics.BookingNote; + +/** + * 结算单管理服务接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface AccountService { + + int querySimilarAccountCodeCount( String partnerCode ) throws RuntimeException; + + /** + * 根据标识获取结算单数据。 + * @param accountId 结算单标识 + * @return 标识为 accountId 的结算单 + * @throws RuntimeException + */ + Account loadAccountById( String accountId ) throws RuntimeException; + + /*** + * 保存结算单。 + * @param account 需要保存的结算单对象 + * @return + * @throws RuntimeException + */ + Account saveAccount( Account account, List rates ) throws RuntimeException; + + /*** + * 更新结算单的状态 + * @param account 需要更新的结算单,且已经设置好了状态值 + * @return + * @throws RuntimeException + */ + Account updateAccountState( Account account ) throws RuntimeException; + + Account removeInvoice( Account account ) throws RuntimeException; + + /** + * 获取结算单对应货币汇率列表 + * @param account 结算单 + * @return + * @throws RuntimeException + */ + List loadAccountCurrencyRates( Account account ) throws RuntimeException; + + /** + * 获取结算单对应的业务托单信息 + * @param account 结算单 + * @return + * @throws RuntimeException + */ + List loadAccountBookingNotes( Account account ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/ChargeService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/ChargeService.java new file mode 100644 index 0000000..283cbc9 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/ChargeService.java @@ -0,0 +1,100 @@ +package com.ksa.service.finance; + +import java.util.List; + +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; + +/** + * 费用数据管理服务接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface ChargeService { + + /** + * 获取托单对应的费用信息。 + * @param bookingNoteId 托单标识 + * @return 收入与支出费用混合在一起的费用列表。 + * @throws RuntimeException + */ + List loadBookingNoteCharges( String bookingNoteId ) throws RuntimeException; + + /** + * 获取托单对应的费用信息。 + * @param bookingNoteId 托单标识 + * @return 收入与支出费用混合在一起的费用列表。 + * @throws RuntimeException + */ + List loadBookingNoteCharges( String bookingNoteId, int direction, int nature ) throws RuntimeException; + + + /** + * 获取托单对应的费用信息,并将收入与支出分别放入传入的收入列表和支出列表中。 + * @param bookingNoteId 托单标识 + * @param incomes 存放查询所获取的收入费用列表 + * @param expenses 存放查询所获取的支出费用列表 + * @return 收入与支出费用混合在一起的费用列表。 + * @throws RuntimeException + */ + List loadBookingNoteCharges( String bookingNoteId, List incomes, List expenses ) throws RuntimeException; + + /** + * 保存托单对应的费用数据。 + * @param note 托单 + * @param incomes 新的收入费用列表 + * @param expenses 新的支出费用列表 + * @return 保存后的收入与支出费用混合在一起的费用列表。 + * @throws RuntimeException + */ + /*List saveBookingNoteCharges( BookingNote note, List incomes, List expenses ) throws RuntimeException;*/ + + /** + * 保存托单对应的费用数据。 + * @param note 托单 + * @param incomes 新的收入费用列表 + * @param expenses 新的支出费用列表 + * @return 保存后的收入与支出费用混合在一起的费用列表。 + * @throws RuntimeException + */ + @Deprecated + List saveBookingNoteCharges( BookingNote note, List incomes, List expenses, int nature ) throws RuntimeException; + // 境内外统一管理 + + /** + * 保存托单对应的费用数据。 + * @param note 托单 + * @param incomes 新的收入费用列表 + * @param expenses 新的支出费用列表 + * @return 保存后的收入与支出费用混合在一起的费用列表。 + * @throws RuntimeException + */ + List saveBookingNoteCharges( BookingNote note, List incomes, List expenses ) throws RuntimeException; + + /** + * 保存托单对应的费用数据。 + * @param note 托单 + * @param incomes 新的收入费用列表 + * @param expenses 新的支出费用列表 + * @return 保存后的收入与支出费用混合在一起的费用列表。 + * @throws RuntimeException + */ + List saveBookingNoteCharges( BookingNote note, List charges, int direction, int nature ) throws RuntimeException; + + /** + * 更新托单的业务状态 + * @param note 托单业务数据,其中包含了托单的标识 和 新的状态标识 + * @return 更新后的托单 + * @throws RuntimeException + */ + BookingNote updateBookingNoteChargeState( BookingNote note ) throws RuntimeException; + + BookingNote updateBookingNoteChargeState( BookingNote note, int direction, int nature ) throws RuntimeException; + + BookingNote updateBookingNoteChargeState( BookingNote note, int nature ) throws RuntimeException; + + List loadBookingNoteCharges( String bookingNoteId, List incomes, List expenses, int nature ) + throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/InvoiceService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/InvoiceService.java new file mode 100644 index 0000000..ee0ce15 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/finance/InvoiceService.java @@ -0,0 +1,23 @@ +package com.ksa.service.finance; + +import com.ksa.model.finance.Account; +import com.ksa.model.finance.Invoice; + + +/** + * 发票管理相关接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface InvoiceService { + + Invoice loadInvoiceById( String id ) throws RuntimeException; + + Invoice assignInvoiceToAccount( Invoice invoice, Account account ) throws RuntimeException; + + Invoice saveInvoice( Invoice invoice ) throws RuntimeException; + + Invoice removeInvoice( Invoice invoice ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/logistics/BookingNoteService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/logistics/BookingNoteService.java new file mode 100644 index 0000000..2f50f17 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/logistics/BookingNoteService.java @@ -0,0 +1,61 @@ +package com.ksa.service.logistics; + +import com.ksa.model.logistics.BookingNote; + +/** + * 托单数据管理服务接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface BookingNoteService { + + /** + * 根据提供了托单类型,获取一个新的空白托单。 + * @param type 托单类型 + * @return 新的空白托单 + * @throws RuntimeException 所提供的托单类型不存在 + */ + BookingNote getNewBookingNote( String type ) throws RuntimeException; + + /** + * 根据标识获取托单数据。 + * @param id 托单标识 + * @return 标识为 id 的托单 + * @throws RuntimeException + */ + BookingNote loadBookingNoteById( String id ) throws RuntimeException; + + /** + * 创建新的托单。 + * @param note 将要被创建的托单 + * @return 创建成功后的托单 + * @throws RuntimeException + */ + BookingNote createBookingNote( BookingNote note ) throws RuntimeException; + + /** + * 更新托单。 + * @param note 将要被更新的托单 + * @return 更新成功后的托单 + * @throws RuntimeException + */ + BookingNote modifyBookingNote( BookingNote note ) throws RuntimeException; + + /** + * 移除托单。 + * @param note 将要被移除的托单 + * @return 移除成功后的托单 + * @throws RuntimeException + */ + BookingNote removeBookingNote( BookingNote note ) throws RuntimeException; + + /** + * 变更托单类型。 + * @param note 将要被变更类型的托单 + * @return 变更类型成功后的托单 + * @throws RuntimeException + */ + BookingNote changeBookingNoteType( BookingNote note ) throws RuntimeException; +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/service/security/SecurityService.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/security/SecurityService.java new file mode 100644 index 0000000..c845ecd --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/service/security/SecurityService.java @@ -0,0 +1,135 @@ +package com.ksa.service.security; + +import com.ksa.model.security.Role; +import com.ksa.model.security.User; + +/** + * 系统安全管理服务接口。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface SecurityService { + + /** + * 根据用户标识获取用户数据。 + * @param userId 用户标识 + * @return 对应标识的用户数据 + * @throws RuntimeException 对应标识的用户不存在 + */ + User loadUserById( String userId ) throws RuntimeException; + + /** + * 创建新用户。 + * @param user 将要被创建的用户数据 + * @return 创建成功后的用户数据 + * @throws RuntimeException + */ + User createUser( User user ) throws RuntimeException; + + /** + * 创建新用户。 + * @param user 将要被创建的用户数据 + * @param roleIds 新用户初始所属角色标识的列表 + * @return 创建成功后的用户数据 + * @throws RuntimeException + */ + User createUser( User user, String[] roleIds ) throws RuntimeException; + + /** + * 锁定用户。 + * @param user 需要被锁定的用户数据 + * @return 锁定后的用户数据 + * @throws RuntimeException + */ + User lockUser( User user ) throws RuntimeException; + + /** + * 激活用户。 + * @param user 需要被激活的用户数据 + * @return 激活后的用户数据 + * @throws RuntimeException + */ + User unlockUser( User user ) throws RuntimeException; + + /** + * 更新用户基本信息(不更新密码)。 + * @param user 新的用户数据 + * @return 更新成功后的用户数据 + * @throws RuntimeException + */ + User modifyUser( User user ) throws RuntimeException; + + /** + * 更新用户密码。 + * @param userId 用户标识 + * @param oldPassword 用户原登录密码 + * @param newPassword 用户新登录密码 + * @return 更新成功后的用户数据 + * @throws RuntimeException + */ + User modifyUser( String userId, String oldPassword, String newPassword ) throws RuntimeException; + + /** + * 更新用户基本信息(包含更新密码)。 + * @param user 新的用户数据 + * @param oldPassword 用户原登录密码 + * @param newPassword 用户新登录密码 + * @return 更新成功后的用户数据 + * @throws RuntimeException + */ + User modifyUser( User user, String oldPassword, String newPassword ) throws RuntimeException; + + /** + * 移除用户数据。 + * @param user 需要被移除的用户数据 + * @return 移除成功后的用户数据 + * @throws RuntimeException + */ + User removeUser( User user ) throws RuntimeException; + + /** + * 根据角色标识获取角色数据。 + * @param roleId 角色标识 + * @return 对应标识的角色数据 + * @throws RuntimeException 对应标识的角色不存在 + */ + Role loadRoleById( String roleId ) throws RuntimeException; + + + /** + * 创建新角色。 + * @param role 将要被创建的角色数据 + * @return 创建成功后的角色数据 + * @throws RuntimeException + */ + Role createRole( Role role ) throws RuntimeException; + + /** + * 创建新角色。 + * @param role 将要被创建的角色数据 + * @param userIds 角色初始所包含的用户标识列表 + * @param permissionIds 角色初始所包含的权限标识列表 + * @return 创建成功后的角色数据 + * @throws RuntimeException + */ + Role createRole( Role role, String[] userIds, String[] permissionIds ) throws RuntimeException; + + /** + * 更新角色基本信息(不更新密码)。 + * @param role 新的角色数据 + * @return 更新成功后的角色数据 + * @throws RuntimeException + */ + Role modifyRole( Role role ) throws RuntimeException; + + /** + * 移除角色数据。 + * @param role 需要被移除的角色数据 + * @return 移除成功后的角色数据 + * @throws RuntimeException + */ + Role removeRole( Role role ) throws RuntimeException; + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/Assert.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/Assert.java new file mode 100644 index 0000000..37207cf --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/Assert.java @@ -0,0 +1,386 @@ +package com.ksa.util; + +import java.util.Collection; +import java.util.Map; + +/** +* Assertion utility class that assists in validating arguments. +* Useful for identifying programmer errors early and clearly at runtime. +* +*

For example, if the contract of a public method states it does not +* allow null arguments, Assert can be used to validate that +* contract. Doing this clearly indicates a contract violation when it +* occurs and protects the class's invariants. +* +*

Typically used to validate method arguments rather than configuration +* properties, to check for cases that are usually programmer errors rather than +* configuration errors. In contrast to config initialization code, there is +* usally no point in falling back to defaults in such methods. +* +*

This class is similar to JUnit's assertion library. If an argument value is +* deemed invalid, an {@link IllegalArgumentException} is thrown (typically). +* For example: +* +*

+* Assert.notNull(clazz, "The class must not be null");
+* Assert.isTrue(i > 0, "The value must be greater than zero");
+* +* Mainly for internal use within the framework; consider Jakarta's Commons Lang +* >= 2.0 for a more comprehensive suite of assertion utilities. +* +* @author Keith Donald +* @author Juergen Hoeller +* @author Colin Sampaleanu +* @author Rob Harrop +* @since 1.1.2 +*/ +public abstract class Assert { + + /** + * Assert a boolean expression, throwing IllegalArgumentException + * if the test result is false. + *
Assert.isTrue(i > 0, "The value must be greater than zero");
+ * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert a boolean expression, throwing IllegalArgumentException + * if the test result is false. + *
Assert.isTrue(i > 0);
+ * @param expression a boolean expression + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression) { + isTrue(expression, "[Assertion failed] - this expression must be true"); + } + + /** + * Assert that an object is null . + *
Assert.isNull(value, "The value must be null");
+ * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is not null + */ + public static void isNull(Object object, String message) { + if (object != null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is null . + *
Assert.isNull(value);
+ * @param object the object to check + * @throws IllegalArgumentException if the object is not null + */ + public static void isNull(Object object) { + isNull(object, "[Assertion failed] - the object argument must be null"); + } + + /** + * Assert that an object is not null . + *
Assert.notNull(clazz, "The class must not be null");
+ * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is not null . + *
Assert.notNull(clazz);
+ * @param object the object to check + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(Object object) { + notNull(object, "[Assertion failed] - this argument is required; it must not be null"); + } + + /** + * Assert that the given String is not empty; that is, + * it must not be null and not the empty String. + *
Assert.hasLength(name, "Name must not be empty");
+ * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see StringUtils#hasLength + */ + public static void hasLength(String text, String message) { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be null and not the empty String. + *
Assert.hasLength(name);
+ * @param text the String to check + * @see StringUtils#hasLength + */ + public static void hasLength(String text) { + hasLength(text, + "[Assertion failed] - this String argument must have length; it must not be null or empty"); + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be null and must contain at least one non-whitespace character. + *
Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see StringUtils#hasText + */ + public static void hasText(String text, String message) { + if (!StringUtils.hasText(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be null and must contain at least one non-whitespace character. + *
Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check + * @see StringUtils#hasText + */ + public static void hasText(String text) { + hasText(text, + "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + } + + /** + * Assert that the given text does not contain the given substring. + *
Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
+ * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails + */ + public static void doesNotContain(String textToSearch, String substring, String message) { + if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && + textToSearch.indexOf(substring) != -1) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given text does not contain the given substring. + *
Assert.doesNotContain(name, "rod");
+ * @param textToSearch the text to search + * @param substring the substring to find within the text + */ + public static void doesNotContain(String textToSearch, String substring) { + doesNotContain(textToSearch, substring, + "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + } + + + /** + * Assert that an array has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(array, "The array must have elements");
+ * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array is null or has no elements + */ + public static void notEmpty(Object[] array, String message) { + if (ObjectUtils.isEmpty(array)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(array);
+ * @param array the array to check + * @throws IllegalArgumentException if the object array is null or has no elements + */ + public static void notEmpty(Object[] array) { + notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + *
Assert.noNullElements(array, "The array must have non-null elements");
+ * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array contains a null element + */ + public static void noNullElements(Object[] array, String message) { + if (array != null) { + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + *
Assert.noNullElements(array);
+ * @param array the array to check + * @throws IllegalArgumentException if the object array contains a null element + */ + public static void noNullElements(Object[] array) { + noNullElements(array, "[Assertion failed] - this array must not contain any null elements"); + } + + /** + * Assert that a collection has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the collection is null or has no elements + */ + public static void notEmpty(Collection collection, String message) { + if (CollectionUtils.isEmpty(collection)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a collection has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check + * @throws IllegalArgumentException if the collection is null or has no elements + */ + public static void notEmpty(Collection collection) { + notEmpty(collection, + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that a Map has entries; that is, it must not be null + * and must have at least one entry. + *
Assert.notEmpty(map, "Map must have entries");
+ * @param map the map to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the map is null or has no entries + */ + public static void notEmpty(Map map, String message) { + if (CollectionUtils.isEmpty(map)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a Map has entries; that is, it must not be null + * and must have at least one entry. + *
Assert.notEmpty(map);
+ * @param map the map to check + * @throws IllegalArgumentException if the map is null or has no entries + */ + public static void notEmpty(Map map) { + notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry"); + } + + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo);
+ * @param clazz the required class + * @param obj the object to check + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(Class clazz, Object obj) { + isInstanceOf(clazz, obj, ""); + } + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo);
+ * @param type the type to check against + * @param obj the object to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(Class type, Object obj, String message) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + throw new IllegalArgumentException(message + + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + + "] must be an instance of " + type); + } + } + + /** + * Assert that superType.isAssignableFrom(subType) is true. + *
Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check + * @param subType the sub type to check + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, Class subType) { + isAssignable(superType, subType, ""); + } + + /** + * Assert that superType.isAssignableFrom(subType) is true. + *
Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check against + * @param subType the sub type to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, Class subType, String message) { + notNull(superType, "Type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + throw new IllegalArgumentException(message + subType + " is not assignable to " + superType); + } + } + + + /** + * Assert a boolean expression, throwing IllegalStateException + * if the test result is false. Call isTrue if you wish to + * throw IllegalArgumentException on an assertion failure. + *
Assert.state(id == null, "The id property must not already be initialized");
+ * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalStateException if expression is false + */ + public static void state(boolean expression, String message) { + if (!expression) { + throw new IllegalStateException(message); + } + } + + /** + * Assert a boolean expression, throwing {@link IllegalStateException} + * if the test result is false. + *

Call {@link #isTrue(boolean)} if you wish to + * throw {@link IllegalArgumentException} on an assertion failure. + *

Assert.state(id == null);
+ * @param expression a boolean expression + * @throws IllegalStateException if the supplied expression is false + */ + public static void state(boolean expression) { + state(expression, "[Assertion failed] - this state invariant must be true"); + } + +} + diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ClassUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ClassUtils.java new file mode 100644 index 0000000..46ef3d6 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ClassUtils.java @@ -0,0 +1,1058 @@ +package com.ksa.util; + +import java.beans.Introspector; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Miscellaneous class utility methods. Mainly for internal use within the + * framework; consider + *
Apache Commons Lang + * for a more comprehensive suite of class utilities. + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Sam Brannen + * @since 1.1 + * @see TypeUtils + * @see ReflectionUtils + */ +@SuppressWarnings("unchecked") +public abstract class ClassUtils { + + /** Suffix for array class names: "[]" */ + public static final String ARRAY_SUFFIX = "[]"; + + /** Prefix for internal array class names: "[" */ + private static final String INTERNAL_ARRAY_PREFIX = "["; + + /** Prefix for internal non-primitive array class names: "[L" */ + private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; + + /** The package separator character '.' */ + private static final char PACKAGE_SEPARATOR = '.'; + + /** The inner class separator character '$' */ + private static final char INNER_CLASS_SEPARATOR = '$'; + + /** The CGLIB class separator character "$$" */ + public static final String CGLIB_CLASS_SEPARATOR = "$$"; + + /** The ".class" file suffix */ + public static final String CLASS_FILE_SUFFIX = ".class"; + + + /** + * Map with primitive wrapper type as key and corresponding primitive + * type as value, for example: Integer.class -> int.class. + */ + private static final Map, Class> primitiveWrapperTypeMap = new HashMap, Class>(8); + + /** + * Map with primitive type as key and corresponding wrapper + * type as value, for example: int.class -> Integer.class. + */ + private static final Map, Class> primitiveTypeToWrapperMap = new HashMap, Class>(8); + + /** + * Map with primitive type name as key and corresponding primitive + * type as value, for example: "int" -> "int.class". + */ + private static final Map> primitiveTypeNameMap = new HashMap>(16); + + /** + * Map with common "java.lang" class name as key and corresponding Class as value. + * Primarily for efficient deserialization of remote invocations. + */ + private static final Map> commonClassCache = new HashMap>(32); + + + static { + primitiveWrapperTypeMap.put(Boolean.class, boolean.class); + primitiveWrapperTypeMap.put(Byte.class, byte.class); + primitiveWrapperTypeMap.put(Character.class, char.class); + primitiveWrapperTypeMap.put(Double.class, double.class); + primitiveWrapperTypeMap.put(Float.class, float.class); + primitiveWrapperTypeMap.put(Integer.class, int.class); + primitiveWrapperTypeMap.put(Long.class, long.class); + primitiveWrapperTypeMap.put(Short.class, short.class); + + for (Map.Entry, Class> entry : primitiveWrapperTypeMap.entrySet()) { + primitiveTypeToWrapperMap.put(entry.getValue(), entry.getKey()); + registerCommonClasses(entry.getKey()); + } + + Set> primitiveTypes = new HashSet>(16); + primitiveTypes.addAll(primitiveWrapperTypeMap.values()); + primitiveTypes.addAll(Arrays.asList( + boolean[].class, byte[].class, char[].class, double[].class, + float[].class, int[].class, long[].class, short[].class)); + for (Class primitiveType : primitiveTypes) { + primitiveTypeNameMap.put(primitiveType.getName(), primitiveType); + } + + registerCommonClasses(Boolean[].class, Byte[].class, Character[].class, Double[].class, + Float[].class, Integer[].class, Long[].class, Short[].class); + registerCommonClasses(Number.class, Number[].class, String.class, String[].class, + Object.class, Object[].class, Class.class, Class[].class); + registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, + Error.class, StackTraceElement.class, StackTraceElement[].class); + } + + + /** + * Register the given common classes with the ClassUtils cache. + */ + private static void registerCommonClasses(Class... commonClasses) { + for (Class clazz : commonClasses) { + commonClassCache.put(clazz.getName(), clazz); + } + } + + /** + * Return the default ClassLoader to use: typically the thread context + * ClassLoader, if available; the ClassLoader that loaded the ClassUtils + * class will be used as fallback. + *

Call this method if you intend to use the thread context ClassLoader + * in a scenario where you absolutely need a non-null ClassLoader reference: + * for example, for class path resource loading (but not necessarily for + * Class.forName, which accepts a null ClassLoader + * reference as well). + * @return the default ClassLoader (never null) + * @see java.lang.Thread#getContextClassLoader() + */ + public static ClassLoader getDefaultClassLoader() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } + catch (Throwable ex) { + // Cannot access thread context ClassLoader - falling back to system class loader... + } + if (cl == null) { + // No thread context class loader -> use class loader of this class. + cl = ClassUtils.class.getClassLoader(); + } + return cl; + } + + /** + * Override the thread context ClassLoader with the environment's bean ClassLoader + * if necessary, i.e. if the bean ClassLoader is not equivalent to the thread + * context ClassLoader already. + * @param classLoaderToUse the actual ClassLoader to use for the thread context + * @return the original thread context ClassLoader, or null if not overridden + */ + public static ClassLoader overrideThreadContextClassLoader(ClassLoader classLoaderToUse) { + Thread currentThread = Thread.currentThread(); + ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); + if (classLoaderToUse != null && !classLoaderToUse.equals(threadContextClassLoader)) { + currentThread.setContextClassLoader(classLoaderToUse); + return threadContextClassLoader; + } + else { + return null; + } + } + + /** + * Replacement for Class.forName() that also returns Class instances + * for primitives (like "int") and array class names (like "String[]"). + *

Always uses the default class loader: that is, preferably the thread context + * class loader, or the ClassLoader that loaded the ClassUtils class as fallback. + * @param name the name of the Class + * @return Class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see Class#forName(String, boolean, ClassLoader) + * @see #getDefaultClassLoader() + * @deprecated as of Spring 3.0, in favor of specifying a ClassLoader explicitly: + * see {@link #forName(String, ClassLoader)} + */ + @Deprecated + public static Class forName(String name) throws ClassNotFoundException, LinkageError { + return forName(name, getDefaultClassLoader()); + } + + /** + * Replacement for Class.forName() that also returns Class instances + * for primitives (e.g."int") and array class names (e.g. "String[]"). + * Furthermore, it is also capable of resolving inner class names in Java source + * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State"). + * @param name the name of the Class + * @param classLoader the class loader to use + * (may be null, which indicates the default class loader) + * @return Class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see Class#forName(String, boolean, ClassLoader) + */ + public static Class forName(String name, ClassLoader classLoader) throws ClassNotFoundException, LinkageError { + Assert.notNull(name, "Name must not be null"); + + Class clazz = resolvePrimitiveClassName(name); + if (clazz == null) { + clazz = commonClassCache.get(name); + } + if (clazz != null) { + return clazz; + } + + // "java.lang.String[]" style arrays + if (name.endsWith(ARRAY_SUFFIX)) { + String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length()); + Class elementClass = forName(elementClassName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[Ljava.lang.String;" style arrays + if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { + String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); + Class elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[[I" or "[[Ljava.lang.String;" style arrays + if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { + String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); + Class elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = getDefaultClassLoader(); + } + try { + return classLoaderToUse.loadClass(name); + } + catch (ClassNotFoundException ex) { + int lastDotIndex = name.lastIndexOf('.'); + if (lastDotIndex != -1) { + String innerClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1); + try { + return classLoaderToUse.loadClass(innerClassName); + } + catch (ClassNotFoundException ex2) { + // swallow - let original exception get through + } + } + throw ex; + } + } + + /** + * Resolve the given class name into a Class instance. Supports + * primitives (like "int") and array class names (like "String[]"). + *

This is effectively equivalent to the forName + * method with the same arguments, with the only difference being + * the exceptions thrown in case of class loading failure. + * @param className the name of the Class + * @param classLoader the class loader to use + * (may be null, which indicates the default class loader) + * @return Class instance for the supplied name + * @throws IllegalArgumentException if the class name was not resolvable + * (that is, the class could not be found or the class file could not be loaded) + * @see #forName(String, ClassLoader) + */ + public static Class resolveClassName(String className, ClassLoader classLoader) throws IllegalArgumentException { + try { + return forName(className, classLoader); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Cannot find class [" + className + "]", ex); + } + catch (LinkageError ex) { + throw new IllegalArgumentException( + "Error loading class [" + className + "]: problem with class file or dependent class.", ex); + } + } + + /** + * Resolve the given class name as primitive class, if appropriate, + * according to the JVM's naming rules for primitive classes. + *

Also supports the JVM's internal class names for primitive arrays. + * Does not support the "[]" suffix notation for primitive arrays; + * this is only supported by {@link #forName(String, ClassLoader)}. + * @param name the name of the potentially primitive class + * @return the primitive class, or null if the name does not denote + * a primitive class or primitive array class + */ + public static Class resolvePrimitiveClassName(String name) { + Class result = null; + // Most class names will be quite long, considering that they + // SHOULD sit in a package, so a length check is worthwhile. + if (name != null && name.length() <= 8) { + // Could be a primitive - likely. + result = primitiveTypeNameMap.get(name); + } + return result; + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return false if either the class or + * one of its dependencies is not present or cannot be loaded. + * @param className the name of the class to check + * @return whether the specified class is present + * @deprecated as of Spring 2.5, in favor of {@link #isPresent(String, ClassLoader)} + */ + @Deprecated + public static boolean isPresent(String className) { + return isPresent(className, getDefaultClassLoader()); + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return false if either the class or + * one of its dependencies is not present or cannot be loaded. + * @param className the name of the class to check + * @param classLoader the class loader to use + * (may be null, which indicates the default class loader) + * @return whether the specified class is present + */ + public static boolean isPresent(String className, ClassLoader classLoader) { + try { + forName(className, classLoader); + return true; + } + catch (Throwable ex) { + // Class or one of its dependencies is not present... + return false; + } + } + + /** + * Return the user-defined class for the given instance: usually simply + * the class of the given instance, but the original class in case of a + * CGLIB-generated subclass. + * @param instance the instance to check + * @return the user-defined class + */ + public static Class getUserClass(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getUserClass(instance.getClass()); + } + + /** + * Return the user-defined class for the given class: usually simply the given + * class, but the original class in case of a CGLIB-generated subclass. + * @param clazz the class to check + * @return the user-defined class + */ + public static Class getUserClass(Class clazz) { + return (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR) ? + clazz.getSuperclass() : clazz); + } + + /** + * Check whether the given class is cache-safe in the given context, + * i.e. whether it is loaded by the given ClassLoader or a parent of it. + * @param clazz the class to analyze + * @param classLoader the ClassLoader to potentially cache metadata in + */ + public static boolean isCacheSafe(Class clazz, ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + ClassLoader target = clazz.getClassLoader(); + if (target == null) { + return false; + } + ClassLoader cur = classLoader; + if (cur == target) { + return true; + } + while (cur != null) { + cur = cur.getParent(); + if (cur == target) { + return true; + } + } + return false; + } + + + /** + * Get the class name without the qualified package name. + * @param className the className to get the short name for + * @return the class name of the class without the package name + * @throws IllegalArgumentException if the className is empty + */ + public static String getShortName(String className) { + Assert.hasLength(className, "Class name must not be empty"); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR); + if (nameEndIndex == -1) { + nameEndIndex = className.length(); + } + String shortName = className.substring(lastDotIndex + 1, nameEndIndex); + shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR); + return shortName; + } + + /** + * Get the class name without the qualified package name. + * @param clazz the class to get the short name for + * @return the class name of the class without the package name + */ + public static String getShortName(Class clazz) { + return getShortName(getQualifiedName(clazz)); + } + + /** + * Return the short string name of a Java class in uncapitalized JavaBeans + * property format. Strips the outer class name in case of an inner class. + * @param clazz the class + * @return the short name rendered in a standard JavaBeans property format + * @see java.beans.Introspector#decapitalize(String) + */ + public static String getShortNameAsProperty(Class clazz) { + String shortName = ClassUtils.getShortName(clazz); + int dotIndex = shortName.lastIndexOf('.'); + shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName); + return Introspector.decapitalize(shortName); + } + + /** + * Determine the name of the class file, relative to the containing + * package: e.g. "String.class" + * @param clazz the class + * @return the file name of the ".class" file + */ + public static String getClassFileName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + return className.substring(lastDotIndex + 1) + CLASS_FILE_SUFFIX; + } + + /** + * Determine the name of the package of the given class: + * e.g. "java.lang" for the java.lang.String class. + * @param clazz the class + * @return the package name, or the empty String if the class + * is defined in the default package + */ + public static String getPackageName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + return (lastDotIndex != -1 ? className.substring(0, lastDotIndex) : ""); + } + + /** + * Return the qualified name of the given class: usually simply + * the class name, but component type class name + "[]" for arrays. + * @param clazz the class + * @return the qualified name of the class + */ + public static String getQualifiedName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isArray()) { + return getQualifiedNameForArray(clazz); + } + else { + return clazz.getName(); + } + } + + /** + * Build a nice qualified name for an array: + * component type class name + "[]". + * @param clazz the array class + * @return a qualified name for the array class + */ + private static String getQualifiedNameForArray(Class clazz) { + StringBuilder result = new StringBuilder(); + while (clazz.isArray()) { + clazz = clazz.getComponentType(); + result.append(ClassUtils.ARRAY_SUFFIX); + } + result.insert(0, clazz.getName()); + return result.toString(); + } + + /** + * Return the qualified name of the given method, consisting of + * fully qualified interface/class name + "." + method name. + * @param method the method + * @return the qualified name of the method + */ + public static String getQualifiedMethodName(Method method) { + Assert.notNull(method, "Method must not be null"); + return method.getDeclaringClass().getName() + "." + method.getName(); + } + + /** + * Return a descriptive name for the given object's type: usually simply + * the class name, but component type class name + "[]" for arrays, + * and an appended list of implemented interfaces for JDK proxies. + * @param value the value to introspect + * @return the qualified name of the class + */ + public static String getDescriptiveType(Object value) { + if (value == null) { + return null; + } + Class clazz = value.getClass(); + if (Proxy.isProxyClass(clazz)) { + StringBuilder result = new StringBuilder(clazz.getName()); + result.append(" implementing "); + Class[] ifcs = clazz.getInterfaces(); + for (int i = 0; i < ifcs.length; i++) { + result.append(ifcs[i].getName()); + if (i < ifcs.length - 1) { + result.append(','); + } + } + return result.toString(); + } + else if (clazz.isArray()) { + return getQualifiedNameForArray(clazz); + } + else { + return clazz.getName(); + } + } + + /** + * Check whether the given class matches the user-specified type name. + * @param clazz the class to check + * @param typeName the type name to match + */ + public static boolean matchesTypeName(Class clazz, String typeName) { + return (typeName != null && + (typeName.equals(clazz.getName()) || typeName.equals(clazz.getSimpleName()) || + (clazz.isArray() && typeName.equals(getQualifiedNameForArray(clazz))))); + } + + + /** + * Determine whether the given class has a public constructor with the given signature. + *

Essentially translates NoSuchMethodException to "false". + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding constructor + * @see java.lang.Class#getMethod + */ + public static boolean hasConstructor(Class clazz, Class... paramTypes) { + return (getConstructorIfAvailable(clazz, paramTypes) != null); + } + + /** + * Determine whether the given class has a public constructor with the given signature, + * and return it if available (else return null). + *

Essentially translates NoSuchMethodException to null. + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return the constructor, or null if not found + * @see java.lang.Class#getConstructor + */ + public static Constructor getConstructorIfAvailable(Class clazz, Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + try { + return clazz.getConstructor(paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * Determine whether the given class has a method with the given signature. + *

Essentially translates NoSuchMethodException to "false". + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding method + * @see java.lang.Class#getMethod + */ + public static boolean hasMethod(Class clazz, String methodName, Class... paramTypes) { + return (getMethodIfAvailable(clazz, methodName, paramTypes) != null); + } + + /** + * Determine whether the given class has a method with the given signature, + * and return it if available (else return null). + *

Essentially translates NoSuchMethodException to null. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * @return the method, or null if not found + * @see java.lang.Class#getMethod + */ + public static Method getMethodIfAvailable(Class clazz, String methodName, Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * Return the number of methods with a given name (with any argument types), + * for the given class and/or its superclasses. Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return the number of methods with the given name + */ + public static int getMethodCountForName(Class clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + int count = 0; + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (methodName.equals(method.getName())) { + count++; + } + } + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + count += getMethodCountForName(ifc, methodName); + } + if (clazz.getSuperclass() != null) { + count += getMethodCountForName(clazz.getSuperclass(), methodName); + } + return count; + } + + /** + * Does the given class or one of its superclasses at least have one or more + * methods with the supplied name (with any argument types)? + * Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return whether there is at least one method with the given name + */ + public static boolean hasAtLeastOneMethodWithName(Class clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (method.getName().equals(methodName)) { + return true; + } + } + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + if (hasAtLeastOneMethodWithName(ifc, methodName)) { + return true; + } + } + return (clazz.getSuperclass() != null && hasAtLeastOneMethodWithName(clazz.getSuperclass(), methodName)); + } + + /** + * Given a method, which may come from an interface, and a target class used + * in the current reflective invocation, find the corresponding target method + * if there is one. E.g. the method may be IFoo.bar() and the + * target class may be DefaultFoo. In this case, the method may be + * DefaultFoo.bar(). This enables attributes on that method to be found. + *

NOTE: In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod}, + * this method does not resolve Java 5 bridge methods automatically. + * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod} + * if bridge method resolution is desirable (e.g. for obtaining metadata from + * the original method definition). + * @param method the method to be invoked, which may come from an interface + * @param targetClass the target class for the current invocation. + * May be null or may not even implement the method. + * @return the specific target method, or the original method if the + * targetClass doesn't implement it or is null + */ + public static Method getMostSpecificMethod(Method method, Class targetClass) { + Method specificMethod = null; + if (method != null && isOverridable(method, targetClass) && + targetClass != null && !targetClass.equals(method.getDeclaringClass())) { + specificMethod = ReflectionUtils.findMethod(targetClass, method.getName(), method.getParameterTypes()); + } + return (specificMethod != null ? specificMethod : method); + } + + /** + * Determine whether the given method is overridable in the given target class. + * @param method the method to check + * @param targetClass the target class to check against + */ + private static boolean isOverridable(Method method, Class targetClass) { + if (Modifier.isPrivate(method.getModifiers())) { + return false; + } + if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { + return true; + } + return getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass)); + } + + /** + * Return a public static method of a class. + * @param methodName the static method name + * @param clazz the class which defines the method + * @param args the parameter types to the method + * @return the static method, or null if no static method was found + * @throws IllegalArgumentException if the method name is blank or the clazz is null + */ + public static Method getStaticMethod(Class clazz, String methodName, Class... args) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + try { + Method method = clazz.getMethod(methodName, args); + return Modifier.isStatic(method.getModifiers()) ? method : null; + } + catch (NoSuchMethodException ex) { + return null; + } + } + + + /** + * Check if the given class represents a primitive wrapper, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper class + */ + public static boolean isPrimitiveWrapper(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return primitiveWrapperTypeMap.containsKey(clazz); + } + + /** + * Check if the given class represents a primitive (i.e. boolean, byte, + * char, short, int, long, float, or double) or a primitive wrapper + * (i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double). + * @param clazz the class to check + * @return whether the given class is a primitive or primitive wrapper class + */ + public static boolean isPrimitiveOrWrapper(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() || isPrimitiveWrapper(clazz)); + } + + /** + * Check if the given class represents an array of primitives, + * i.e. boolean, byte, char, short, int, long, float, or double. + * @param clazz the class to check + * @return whether the given class is a primitive array class + */ + public static boolean isPrimitiveArray(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && clazz.getComponentType().isPrimitive()); + } + + /** + * Check if the given class represents an array of primitive wrappers, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper array class + */ + public static boolean isPrimitiveWrapperArray(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType())); + } + + /** + * Resolve the given class if it is a primitive class, + * returning the corresponding primitive wrapper type instead. + * @param clazz the class to check + * @return the original class, or a primitive wrapper for the original primitive type + */ + public static Class resolvePrimitiveIfNecessary(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() ? primitiveTypeToWrapperMap.get(clazz) : clazz); + } + + /** + * Check if the right-hand side type may be assigned to the left-hand side + * type, assuming setting by reflection. Considers primitive wrapper + * classes as assignable to the corresponding primitive types. + * @param lhsType the target type + * @param rhsType the value type that should be assigned to the target type + * @return if the target type is assignable from the value type + * @see TypeUtils#isAssignable + */ + public static boolean isAssignable(Class lhsType, Class rhsType) { + Assert.notNull(lhsType, "Left-hand side type must not be null"); + Assert.notNull(rhsType, "Right-hand side type must not be null"); + return (lhsType.isAssignableFrom(rhsType) || + lhsType.equals(primitiveWrapperTypeMap.get(rhsType))); + } + + /** + * Determine if the given type is assignable from the given value, + * assuming setting by reflection. Considers primitive wrapper classes + * as assignable to the corresponding primitive types. + * @param type the target type + * @param value the value that should be assigned to the type + * @return if the type is assignable from the value + */ + public static boolean isAssignableValue(Class type, Object value) { + Assert.notNull(type, "Type must not be null"); + return (value != null ? isAssignable(type, value.getClass()) : !type.isPrimitive()); + } + + + /** + * Convert a "/"-based resource path to a "."-based fully qualified class name. + * @param resourcePath the resource path pointing to a class + * @return the corresponding fully qualified class name + */ + public static String convertResourcePathToClassName(String resourcePath) { + Assert.notNull(resourcePath, "Resource path must not be null"); + return resourcePath.replace('/', '.'); + } + + /** + * Convert a "."-based fully qualified class name to a "/"-based resource path. + * @param className the fully qualified class name + * @return the corresponding resource path, pointing to the class + */ + public static String convertClassNameToResourcePath(String className) { + Assert.notNull(className, "Class name must not be null"); + return className.replace('.', '/'); + } + + /** + * Return a path suitable for use with ClassLoader.getResource + * (also suitable for use with Class.getResource by prepending a + * slash ('/') to the return value). Built by taking the package of the specified + * class file, converting all dots ('.') to slashes ('/'), adding a trailing slash + * if necessary, and concatenating the specified resource name to this. + *
As such, this function may be used to build a path suitable for + * loading a resource file that is in the same package as a class file, + * although {@link org.springframework.core.io.ClassPathResource} is usually + * even more convenient. + * @param clazz the Class whose package will be used as the base + * @param resourceName the resource name to append. A leading slash is optional. + * @return the built-up resource path + * @see java.lang.ClassLoader#getResource + * @see java.lang.Class#getResource + */ + public static String addResourcePathToPackagePath(Class clazz, String resourceName) { + Assert.notNull(resourceName, "Resource name must not be null"); + if (!resourceName.startsWith("/")) { + return classPackageAsResourcePath(clazz) + "/" + resourceName; + } + return classPackageAsResourcePath(clazz) + resourceName; + } + + /** + * Given an input class object, return a string which consists of the + * class's package name as a pathname, i.e., all dots ('.') are replaced by + * slashes ('/'). Neither a leading nor trailing slash is added. The result + * could be concatenated with a slash and the name of a resource and fed + * directly to ClassLoader.getResource(). For it to be fed to + * Class.getResource instead, a leading slash would also have + * to be prepended to the returned value. + * @param clazz the input class. A null value or the default + * (empty) package will result in an empty string ("") being returned. + * @return a path which represents the package name + * @see ClassLoader#getResource + * @see Class#getResource + */ + public static String classPackageAsResourcePath(Class clazz) { + if (clazz == null) { + return ""; + } + String className = clazz.getName(); + int packageEndIndex = className.lastIndexOf('.'); + if (packageEndIndex == -1) { + return ""; + } + String packageName = className.substring(0, packageEndIndex); + return packageName.replace('.', '/'); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given array. + *

Basically like AbstractCollection.toString(), but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be null) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(Class... classes) { + return classNamesToString(Arrays.asList(classes)); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given collection. + *

Basically like AbstractCollection.toString(), but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be null) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(Collection> classes) { + if (CollectionUtils.isEmpty(classes)) { + return "[]"; + } + StringBuilder sb = new StringBuilder("["); + for (Iterator> it = classes.iterator(); it.hasNext(); ) { + Class clazz = it.next(); + sb.append(clazz.getName()); + if (it.hasNext()) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + + + /** + * Return all interfaces that the given instance implements as array, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as array + */ + public static Class[] getAllInterfaces(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClass(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as array, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as array + */ + public static Class[] getAllInterfacesForClass(Class clazz) { + return getAllInterfacesForClass(clazz, null); + } + + /** + * Return all interfaces that the given class implements as array, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be null when accepting all declared interfaces) + * @return all interfaces that the given object implements as array + */ + public static Class[] getAllInterfacesForClass(Class clazz, ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface()) { + return new Class[] {clazz}; + } + List> interfaces = new ArrayList>(); + while (clazz != null) { + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + if (!interfaces.contains(ifc) && + (classLoader == null || isVisible(ifc, classLoader))) { + interfaces.add(ifc); + } + } + clazz = clazz.getSuperclass(); + } + return interfaces.toArray(new Class[interfaces.size()]); + } + + /** + * Return all interfaces that the given instance implements as Set, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as Set + */ + @SuppressWarnings( "rawtypes" ) + public static Set getAllInterfacesAsSet(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClassAsSet(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as Set, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as Set + */ + @SuppressWarnings( "rawtypes" ) + public static Set getAllInterfacesForClassAsSet(Class clazz) { + return getAllInterfacesForClassAsSet(clazz, null); + } + + /** + * Return all interfaces that the given class implements as Set, + * including ones implemented by superclasses. + *

If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be null when accepting all declared interfaces) + * @return all interfaces that the given object implements as Set + */ + @SuppressWarnings( "rawtypes" ) + public static Set getAllInterfacesForClassAsSet(Class clazz, ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface()) { + return Collections.singleton(clazz); + } + Set interfaces = new LinkedHashSet(); + while (clazz != null) { + for (int i = 0; i < clazz.getInterfaces().length; i++) { + Class ifc = clazz.getInterfaces()[i]; + if (classLoader == null || isVisible(ifc, classLoader)) { + interfaces.add(ifc); + } + } + clazz = clazz.getSuperclass(); + } + return interfaces; + } + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

This implementation builds a JDK proxy class for the given interfaces. + * @param interfaces the interfaces to merge + * @param classLoader the ClassLoader to create the composite Class in + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + public static Class createCompositeInterface(Class[] interfaces, ClassLoader classLoader) { + Assert.notEmpty(interfaces, "Interfaces must not be empty"); + Assert.notNull(classLoader, "ClassLoader must not be null"); + return Proxy.getProxyClass(classLoader, interfaces); + } + + /** + * Check whether the given class is visible in the given ClassLoader. + * @param clazz the class to check (typically an interface) + * @param classLoader the ClassLoader to check against (may be null, + * in which case this method will always return true) + */ + public static boolean isVisible(Class clazz, ClassLoader classLoader) { + if (classLoader == null) { + return true; + } + try { + Class actualClass = classLoader.loadClass(clazz.getName()); + return (clazz == actualClass); + // Else: different interface class found... + } + catch (ClassNotFoundException ex) { + // No interface class found... + return false; + } + } + +} \ No newline at end of file diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/CollectionUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/CollectionUtils.java new file mode 100644 index 0000000..c758c21 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/CollectionUtils.java @@ -0,0 +1,296 @@ +package com.ksa.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Miscellaneous collection utility methods. + * Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 1.1.3 + */ +public abstract class CollectionUtils { + + /** + * Return true if the supplied Collection is null + * or empty. Otherwise, return false. + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Return true if the supplied Map is null + * or empty. Otherwise, return false. + * @param map the Map to check + * @return whether the given Map is empty + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + /** + * Convert the supplied array into a List. A primitive array gets + * converted into a List of the appropriate wrapper type. + *

A null source value will be converted to an + * empty List. + * @param source the (potentially primitive) array + * @return the converted List result + * @see ObjectUtils#toObjectArray(Object) + */ + public static List arrayToList(Object source) { + return Arrays.asList(ObjectUtils.toObjectArray(source)); + } + + /** + * Merge the given array into the given Collection. + * @param array the array to merge (may be null) + * @param collection the target Collection to merge the array into + */ + @SuppressWarnings({ "unchecked", "rawtypes" } ) + public static void mergeArrayIntoCollection(Object array, Collection collection) { + if (collection == null) { + throw new IllegalArgumentException("Collection must not be null"); + } + Object[] arr = ObjectUtils.toObjectArray(array); + for (Object elem : arr) { + collection.add( elem ); + } + } + + /** + * Merge the given Properties instance into the given Map, + * copying all properties (key-value pairs) over. + *

Uses Properties.propertyNames() to even catch + * default properties linked into the original Properties instance. + * @param props the Properties instance to merge (may be null) + * @param map the target Map to merge the properties into + */ + public static void mergePropertiesIntoMap(Properties props, Map map) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + if (props != null) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String key = (String) en.nextElement(); + Object value = props.getProperty(key); + if (value == null) { + // Potentially a non-String value... + value = props.get(key); + } + map.put(key, value); + } + } + } + + + /** + * Check whether the given Iterator contains the given element. + * @param iterator the Iterator to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(Iterator iterator, Object element) { + if (iterator != null) { + while (iterator.hasNext()) { + Object candidate = iterator.next(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Enumeration contains the given element. + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(Enumeration enumeration, Object element) { + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + Object candidate = enumeration.nextElement(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Collection contains the given element instance. + *

Enforces the given instance to be present, rather than returning + * true for an equal element as well. + * @param collection the Collection to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean containsInstance(Collection collection, Object element) { + if (collection != null) { + for (Object candidate : collection) { + if (candidate == element) { + return true; + } + } + } + return false; + } + + /** + * Return true if any element in 'candidates' is + * contained in 'source'; otherwise returns false. + * @param source the source Collection + * @param candidates the candidates to search for + * @return whether any of the candidates has been found + */ + public static boolean containsAny(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return false; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return true; + } + } + return false; + } + + /** + * Return the first element in 'candidates' that is contained in + * 'source'. If no element in 'candidates' is present in + * 'source' returns null. Iteration order is + * {@link Collection} implementation specific. + * @param source the source Collection + * @param candidates the candidates to search for + * @return the first present object, or null if not found + */ + public static Object findFirstMatch(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return null; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return candidate; + } + } + return null; + } + + /** + * Find a single value of the given type in the given Collection. + * @param collection the Collection to search + * @param type the type to look for + * @return a value of the given type found if there is a clear match, + * or null if none or more than one such value found + */ + @SuppressWarnings("unchecked") + public static T findValueOfType(Collection collection, Class type) { + if (isEmpty(collection)) { + return null; + } + T value = null; + for (Object element : collection) { + if (type == null || type.isInstance(element)) { + if (value != null) { + // More than one value found... no clear single value. + return null; + } + value = (T) element; + } + } + return value; + } + + /** + * Find a single value of one of the given types in the given Collection: + * searching the Collection for a value of the first type, then + * searching for a value of the second type, etc. + * @param collection the collection to search + * @param types the types to look for, in prioritized order + * @return a value of one of the given types found if there is a clear match, + * or null if none or more than one such value found + */ + public static Object findValueOfType(Collection collection, Class[] types) { + if (isEmpty(collection) || ObjectUtils.isEmpty(types)) { + return null; + } + for (Class type : types) { + Object value = findValueOfType(collection, type); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Determine whether the given Collection only contains a single unique object. + * @param collection the Collection to check + * @return true if the collection contains a single reference or + * multiple references to the same instance, false else + */ + public static boolean hasUniqueObject(Collection collection) { + if (isEmpty(collection)) { + return false; + } + boolean hasCandidate = false; + Object candidate = null; + for (Object elem : collection) { + if (!hasCandidate) { + hasCandidate = true; + candidate = elem; + } + else if (candidate != elem) { + return false; + } + } + return true; + } + + /** + * Adapts an enumeration to an iterator. + * @param enumeration the enumeration + * @return the iterator + */ + public static Iterator toIterator(Enumeration enumeration) { + return new EnumerationIterator(enumeration); + } + + + /** + * Iterator wrapping an Enumeration. + */ + private static class EnumerationIterator implements Iterator { + + private Enumeration enumeration; + + public EnumerationIterator(Enumeration enumeration) { + this.enumeration = enumeration; + } + + public boolean hasNext() { + return this.enumeration.hasMoreElements(); + } + + public E next() { + return this.enumeration.nextElement(); + } + + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported"); + } + } + +} \ No newline at end of file diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ObjectUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ObjectUtils.java new file mode 100644 index 0000000..82af833 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ObjectUtils.java @@ -0,0 +1,830 @@ +package com.ksa.util; + +import java.lang.reflect.Array; +import java.util.Arrays; + +/** + * Miscellaneous object utility methods. + * + *

Mainly for internal use within the framework; consider + * Jakarta's Commons Lang + * for a more comprehensive suite of object utilities. + * + *

Thanks to Alex Ruiz for contributing several enhancements to this class! + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rod Johnson + * @author Rob Harrop + * @since 19.03.2004 + * @see org.apache.commons.lang.ObjectUtils + */ +public abstract class ObjectUtils { + + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + + + /** + * Return whether the given throwable is a checked exception: + * that is, neither a RuntimeException nor an Error. + * @param ex the throwable to check + * @return whether the throwable is a checked exception + * @see java.lang.Exception + * @see java.lang.RuntimeException + * @see java.lang.Error + */ + public static boolean isCheckedException(Throwable ex) { + return !(ex instanceof RuntimeException || ex instanceof Error); + } + + /** + * Check whether the given exception is compatible with the exceptions + * declared in a throws clause. + * @param ex the exception to checked + * @param declaredExceptions the exceptions declared in the throws clause + * @return whether the given exception is compatible + */ + public static boolean isCompatibleWithThrowsClause(Throwable ex, Class[] declaredExceptions) { + if (!isCheckedException(ex)) { + return true; + } + if (declaredExceptions != null) { + int i = 0; + while (i < declaredExceptions.length) { + if (declaredExceptions[i].isAssignableFrom(ex.getClass())) { + return true; + } + i++; + } + } + return false; + } + + /** + * Determine whether the given object is an array: + * either an Object array or a primitive array. + * @param obj the object to check + */ + public static boolean isArray(Object obj) { + return (obj != null && obj.getClass().isArray()); + } + + /** + * Determine whether the given array is empty: + * i.e. null or of zero length. + * @param array the array to check + */ + public static boolean isEmpty(Object[] array) { + return (array == null || array.length == 0); + } + + /** + * Check whether the given array contains the given element. + * @param array the array to check (may be null, + * in which case the return value will always be false) + * @param element the element to check for + * @return whether the element has been found in the given array + */ + public static boolean containsElement(Object[] array, Object element) { + if (array == null) { + return false; + } + for (Object arrayEle : array) { + if (nullSafeEquals(arrayEle, element)) { + return true; + } + } + return false; + } + + /** + * Append the given Object to the given array, returning a new array + * consisting of the input array contents plus the given Object. + * @param array the array to append to (can be null) + * @param obj the Object to append + * @return the new array (of the same component type; never null) + */ + public static Object[] addObjectToArray(Object[] array, Object obj) { + Class compType = Object.class; + if (array != null) { + compType = array.getClass().getComponentType(); + } + else if (obj != null) { + compType = obj.getClass(); + } + int newArrLength = (array != null ? array.length + 1 : 1); + Object[] newArr = (Object[]) Array.newInstance(compType, newArrLength); + if (array != null) { + System.arraycopy(array, 0, newArr, 0, array.length); + } + newArr[newArr.length - 1] = obj; + return newArr; + } + + /** + * Convert the given array (which may be a primitive array) to an + * object array (if necessary of primitive wrapper objects). + *

A null source value will be converted to an + * empty Object array. + * @param source the (potentially primitive) array + * @return the corresponding object array (never null) + * @throws IllegalArgumentException if the parameter is not an array + */ + public static Object[] toObjectArray(Object source) { + if (source instanceof Object[]) { + return (Object[]) source; + } + if (source == null) { + return new Object[0]; + } + if (!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + source); + } + int length = Array.getLength(source); + if (length == 0) { + return new Object[0]; + } + Class wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[]) Array.newInstance(wrapperType, length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + + //--------------------------------------------------------------------- + // Convenience methods for content-based equality/hash-code handling + //--------------------------------------------------------------------- + + /** + * Determine if the given objects are equal, returning true + * if both are null or false if only one is + * null. + *

Compares arrays with Arrays.equals, performing an equality + * check based on the array elements rather than the array reference. + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see java.util.Arrays#equals + */ + public static boolean nullSafeEquals(Object o1, Object o2) { + if (o1 == o2) { + return true; + } + if (o1 == null || o2 == null) { + return false; + } + if (o1.equals(o2)) { + return true; + } + if (o1.getClass().isArray() && o2.getClass().isArray()) { + if (o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[]) o1, (Object[]) o2); + } + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[]) o1, (boolean[]) o2); + } + if (o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[]) o1, (byte[]) o2); + } + if (o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[]) o1, (char[]) o2); + } + if (o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[]) o1, (double[]) o2); + } + if (o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[]) o1, (float[]) o2); + } + if (o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[]) o1, (int[]) o2); + } + if (o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[]) o1, (long[]) o2); + } + if (o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[]) o1, (short[]) o2); + } + } + return false; + } + + /** + * Return as hash code for the given object; typically the value of + * {@link Object#hashCode()}. If the object is an array, + * this method will delegate to any of the nullSafeHashCode + * methods for arrays in this class. If the object is null, + * this method returns 0. + * @see #nullSafeHashCode(Object[]) + * @see #nullSafeHashCode(boolean[]) + * @see #nullSafeHashCode(byte[]) + * @see #nullSafeHashCode(char[]) + * @see #nullSafeHashCode(double[]) + * @see #nullSafeHashCode(float[]) + * @see #nullSafeHashCode(int[]) + * @see #nullSafeHashCode(long[]) + * @see #nullSafeHashCode(short[]) + */ + public static int nullSafeHashCode(Object obj) { + if (obj == null) { + return 0; + } + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return nullSafeHashCode((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeHashCode((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeHashCode((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeHashCode((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeHashCode((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeHashCode((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeHashCode((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeHashCode((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeHashCode((short[]) obj); + } + } + return obj.hashCode(); + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(Object[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + nullSafeHashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(boolean[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(byte[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(char[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(double[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(float[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(int[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(long[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(short[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return the same value as {@link Boolean#hashCode()}. + * @see Boolean#hashCode() + */ + public static int hashCode(boolean bool) { + return bool ? 1231 : 1237; + } + + /** + * Return the same value as {@link Double#hashCode()}. + * @see Double#hashCode() + */ + public static int hashCode(double dbl) { + long bits = Double.doubleToLongBits(dbl); + return hashCode(bits); + } + + /** + * Return the same value as {@link Float#hashCode()}. + * @see Float#hashCode() + */ + public static int hashCode(float flt) { + return Float.floatToIntBits(flt); + } + + /** + * Return the same value as {@link Long#hashCode()}. + * @see Long#hashCode() + */ + public static int hashCode(long lng) { + return (int) (lng ^ (lng >>> 32)); + } + + + //--------------------------------------------------------------------- + // Convenience methods for toString output + //--------------------------------------------------------------------- + + /** + * Return a String representation of an object's overall identity. + * @param obj the object (may be null) + * @return the object's identity as String representation, + * or an empty String if the object was null + */ + public static String identityToString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + /** + * Return a hex String form of an object's identity hash code. + * @param obj the object + * @return the object's identity code in hex notation + */ + public static String getIdentityHexString(Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + /** + * Return a content-based String representation if obj is + * not null; otherwise returns an empty String. + *

Differs from {@link #nullSafeToString(Object)} in that it returns + * an empty String rather than "null" for a null value. + * @param obj the object to build a display String for + * @return a display String representation of obj + * @see #nullSafeToString(Object) + */ + public static String getDisplayString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return nullSafeToString(obj); + } + + /** + * Determine the class name for the given object. + *

Returns "null" if obj is null. + * @param obj the object to introspect (may be null) + * @return the corresponding class name + */ + public static String nullSafeClassName(Object obj) { + return (obj != null ? obj.getClass().getName() : NULL_STRING); + } + + /** + * Return a String representation of the specified Object. + *

Builds a String representation of the contents in case of an array. + * Returns "null" if obj is null. + * @param obj the object to build a String representation for + * @return a String representation of obj + */ + public static String nullSafeToString(Object obj) { + if (obj == null) { + return NULL_STRING; + } + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Object[]) { + return nullSafeToString((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeToString((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeToString((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeToString((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeToString((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeToString((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeToString((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeToString((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeToString((short[]) obj); + } + String str = obj.toString(); + return (str != null ? str : EMPTY_STRING); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(Object[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(String.valueOf(array[i])); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(boolean[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(byte[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(char[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append("'").append(array[i]).append("'"); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(double[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(float[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(int[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(long[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(short[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } + else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ReflectionUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ReflectionUtils.java new file mode 100644 index 0000000..05e7cea --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ReflectionUtils.java @@ -0,0 +1,605 @@ +package com.ksa.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Simple utility class for working with the reflection API and handling + * reflection exceptions. + * + *

Only intended for internal use. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rod Johnson + * @author Costin Leau + * @author Sam Brannen + * @since 1.2.2 + */ +public abstract class ReflectionUtils { + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with + * the supplied name. Searches all superclasses up to + * {@link Object}. + * @param clazz the class to introspect + * @param name the name of the field + * @return the corresponding Field object, or null if not found + */ + public static Field findField(Class clazz, String name) { + return findField(clazz, name, null); + } + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with + * the supplied name and/or {@link Class type}. Searches all + * superclasses up to {@link Object}. + * @param clazz the class to introspect + * @param name the name of the field (may be null if type is specified) + * @param type the type of the field (may be null if name is specified) + * @return the corresponding Field object, or null if not found + */ + public static Field findField(Class clazz, String name, Class type) { + Assert.notNull(clazz, "Class must not be null"); + Assert.isTrue(name != null || type != null, "Either name or type of the field must be specified"); + Class searchType = clazz; + while (!Object.class.equals(searchType) && searchType != null) { + Field[] fields = searchType.getDeclaredFields(); + for (Field field : fields) { + if ((name == null || name.equals(field.getName())) && (type == null || type.equals(field.getType()))) { + return field; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * Set the field represented by the supplied {@link Field field object} on the + * specified {@link Object target object} to the specified value. + * In accordance with {@link Field#set(Object, Object)} semantics, the new value + * is automatically unwrapped if the underlying field has a primitive type. + *

Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}. + * @param field the field to set + * @param target the target object on which to set the field + * @param value the value to set; may be null + */ + public static void setField(Field field, Object target, Object value) { + try { + field.set(target, value); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException("Unexpected reflection exception - " + ex.getClass().getName() + ": " + + ex.getMessage()); + } + } + + /** + * Get the field represented by the supplied {@link Field field object} on the + * specified {@link Object target object}. In accordance with {@link Field#get(Object)} + * semantics, the returned value is automatically wrapped if the underlying field + * has a primitive type. + *

Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}. + * @param field the field to get + * @param target the target object from which to get the field + * @return the field's current value + */ + public static Object getField(Field field, Object target) { + try { + return field.get(target); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException("Unexpected reflection exception - " + ex.getClass().getName() + ": " + + ex.getMessage()); + } + } + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied name + * and no parameters. Searches all superclasses up to Object. + *

Returns null if no {@link Method} can be found. + * @param clazz the class to introspect + * @param name the name of the method + * @return the Method object, or null if none found + */ + public static Method findMethod(Class clazz, String name) { + return findMethod(clazz, name, new Class[0]); + } + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied name + * and parameter types. Searches all superclasses up to Object. + *

Returns null if no {@link Method} can be found. + * @param clazz the class to introspect + * @param name the name of the method + * @param paramTypes the parameter types of the method + * (may be null to indicate any signature) + * @return the Method object, or null if none found + */ + public static Method findMethod(Class clazz, String name, Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(name, "Method name must not be null"); + Class searchType = clazz; + while (searchType != null) { + Method[] methods = (searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods()); + for (Method method : methods) { + if (name.equals(method.getName()) + && (paramTypes == null || Arrays.equals(paramTypes, method.getParameterTypes()))) { + return method; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * Invoke the specified {@link Method} against the supplied target object with no arguments. + * The target object can be null when invoking a static {@link Method}. + *

Thrown exceptions are handled via a call to {@link #handleReflectionException}. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @return the invocation result, if any + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeMethod(Method method, Object target) { + return invokeMethod(method, target, new Object[0]); + } + + /** + * Invoke the specified {@link Method} against the supplied target object with the + * supplied arguments. The target object can be null when invoking a + * static {@link Method}. + *

Thrown exceptions are handled via a call to {@link #handleReflectionException}. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @param args the invocation arguments (may be null) + * @return the invocation result, if any + */ + public static Object invokeMethod(Method method, Object target, Object... args) { + try { + return method.invoke(target, args); + } + catch (Exception ex) { + handleReflectionException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Invoke the specified JDBC API {@link Method} against the supplied target + * object with no arguments. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @return the invocation result, if any + * @throws SQLException the JDBC API SQLException to rethrow (if any) + * @see #invokeJdbcMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeJdbcMethod(Method method, Object target) throws SQLException { + return invokeJdbcMethod(method, target, new Object[0]); + } + + /** + * Invoke the specified JDBC API {@link Method} against the supplied target + * object with the supplied arguments. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @param args the invocation arguments (may be null) + * @return the invocation result, if any + * @throws SQLException the JDBC API SQLException to rethrow (if any) + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + */ + public static Object invokeJdbcMethod(Method method, Object target, Object... args) throws SQLException { + try { + return method.invoke(target, args); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + handleInvocationTargetException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Handle the given reflection exception. Should only be called if no + * checked exception is expected to be thrown by the target method. + *

Throws the underlying RuntimeException or Error in case of an + * InvocationTargetException with such a root cause. Throws an + * IllegalStateException with an appropriate message else. + * @param ex the reflection exception to handle + */ + public static void handleReflectionException(Exception ex) { + if (ex instanceof NoSuchMethodException) { + throw new IllegalStateException("Method not found: " + ex.getMessage()); + } + if (ex instanceof IllegalAccessException) { + throw new IllegalStateException("Could not access method: " + ex.getMessage()); + } + if (ex instanceof InvocationTargetException) { + handleInvocationTargetException((InvocationTargetException) ex); + } + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + handleUnexpectedException(ex); + } + + /** + * Handle the given invocation target exception. Should only be called if no + * checked exception is expected to be thrown by the target method. + *

Throws the underlying RuntimeException or Error in case of such a root + * cause. Throws an IllegalStateException else. + * @param ex the invocation target exception to handle + */ + public static void handleInvocationTargetException(InvocationTargetException ex) { + rethrowRuntimeException(ex.getTargetException()); + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * target exception of an {@link InvocationTargetException}. Should + * only be called if no checked exception is expected to be thrown by the + * target method. + *

Rethrows the underlying exception cast to an {@link RuntimeException} or + * {@link Error} if appropriate; otherwise, throws an + * {@link IllegalStateException}. + * @param ex the exception to rethrow + * @throws RuntimeException the rethrown exception + */ + public static void rethrowRuntimeException(Throwable ex) { + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + handleUnexpectedException(ex); + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * target exception of an {@link InvocationTargetException}. Should + * only be called if no checked exception is expected to be thrown by the + * target method. + *

Rethrows the underlying exception cast to an {@link Exception} or + * {@link Error} if appropriate; otherwise, throws an + * {@link IllegalStateException}. + * @param ex the exception to rethrow + * @throws Exception the rethrown exception (in case of a checked exception) + */ + public static void rethrowException(Throwable ex) throws Exception { + if (ex instanceof Exception) { + throw (Exception) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + handleUnexpectedException(ex); + } + + /** + * Throws an IllegalStateException with the given exception as root cause. + * @param ex the unexpected exception + */ + private static void handleUnexpectedException(Throwable ex) { + throw new IllegalStateException("Unexpected exception thrown", ex); + } + + /** + * Determine whether the given method explicitly declares the given + * exception or one of its superclasses, which means that an exception of + * that type can be propagated as-is within a reflective invocation. + * @param method the declaring method + * @param exceptionType the exception to throw + * @return true if the exception can be thrown as-is; + * false if it needs to be wrapped + */ + public static boolean declaresException(Method method, Class exceptionType) { + Assert.notNull(method, "Method must not be null"); + Class[] declaredExceptions = method.getExceptionTypes(); + for (Class declaredException : declaredExceptions) { + if (declaredException.isAssignableFrom(exceptionType)) { + return true; + } + } + return false; + } + + /** + * Determine whether the given field is a "public static final" constant. + * @param field the field to check + */ + public static boolean isPublicStaticFinal(Field field) { + int modifiers = field.getModifiers(); + return (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)); + } + + /** + * Determine whether the given method is an "equals" method. + * @see java.lang.Object#equals(Object) + */ + public static boolean isEqualsMethod(Method method) { + if (method == null || !method.getName().equals("equals")) { + return false; + } + Class[] paramTypes = method.getParameterTypes(); + return (paramTypes.length == 1 && paramTypes[0] == Object.class); + } + + /** + * Determine whether the given method is a "hashCode" method. + * @see java.lang.Object#hashCode() + */ + public static boolean isHashCodeMethod(Method method) { + return (method != null && method.getName().equals("hashCode") && method.getParameterTypes().length == 0); + } + + /** + * Determine whether the given method is a "toString" method. + * @see java.lang.Object#toString() + */ + public static boolean isToStringMethod(Method method) { + return (method != null && method.getName().equals("toString") && method.getParameterTypes().length == 0); + } + + /** + * Make the given field accessible, explicitly setting it accessible if + * necessary. The setAccessible(true) method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param field the field to make accessible + * @see java.lang.reflect.Field#setAccessible + */ + public static void makeAccessible(Field field) { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) + && !field.isAccessible()) { + field.setAccessible(true); + } + } + + /** + * Make the given method accessible, explicitly setting it accessible if + * necessary. The setAccessible(true) method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param method the method to make accessible + * @see java.lang.reflect.Method#setAccessible + */ + public static void makeAccessible(Method method) { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) { + method.setAccessible(true); + } + } + + /** + * Make the given constructor accessible, explicitly setting it accessible + * if necessary. The setAccessible(true) method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param ctor the constructor to make accessible + * @see java.lang.reflect.Constructor#setAccessible + */ + public static void makeAccessible(Constructor ctor) { + if ((!Modifier.isPublic(ctor.getModifiers()) || !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) + && !ctor.isAccessible()) { + ctor.setAccessible(true); + } + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses. + *

The same named method occurring on subclass and superclass will appear + * twice, unless excluded by a {@link MethodFilter}. + * @param clazz class to start looking at + * @param mc the callback to invoke for each method + * @see #doWithMethods(Class, MethodCallback, MethodFilter) + */ + public static void doWithMethods(Class clazz, MethodCallback mc) throws IllegalArgumentException { + doWithMethods(clazz, mc, null); + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses. + *

The same named method occurring on subclass and superclass will appear + * twice, unless excluded by the specified {@link MethodFilter}. + * @param clazz class to start looking at + * @param mc the callback to invoke for each method + * @param mf the filter that determines the methods to apply the callback to + */ + public static void doWithMethods(Class clazz, MethodCallback mc, MethodFilter mf) + throws IllegalArgumentException { + + // Keep backing up the inheritance hierarchy. + Class targetClass = clazz; + do { + Method[] methods = targetClass.getDeclaredMethods(); + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Shouldn't be illegal to access method '" + method.getName() + + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null); + } + + /** + * Get all declared methods on the leaf class and all superclasses. Leaf + * class methods are included first. + */ + public static Method[] getAllDeclaredMethods(Class leafClass) throws IllegalArgumentException { + final List methods = new ArrayList(32); + doWithMethods(leafClass, new MethodCallback() { + public void doWith(Method method) { + methods.add(method); + } + }); + return methods.toArray(new Method[methods.size()]); + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * @param clazz the target class to analyze + * @param fc the callback to invoke for each field + */ + public static void doWithFields(Class clazz, FieldCallback fc) throws IllegalArgumentException { + doWithFields(clazz, fc, null); + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * @param clazz the target class to analyze + * @param fc the callback to invoke for each field + * @param ff the filter that determines the fields to apply the callback to + */ + public static void doWithFields(Class clazz, FieldCallback fc, FieldFilter ff) + throws IllegalArgumentException { + + // Keep backing up the inheritance hierarchy. + Class targetClass = clazz; + do { + Field[] fields = targetClass.getDeclaredFields(); + for (Field field : fields) { + // Skip static and final fields. + if (ff != null && !ff.matches(field)) { + continue; + } + try { + fc.doWith(field); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException( + "Shouldn't be illegal to access field '" + field.getName() + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + } + + /** + * Given the source object and the destination, which must be the same class + * or a subclass, copy all fields, including inherited fields. Designed to + * work on objects with public no-arg constructors. + * @throws IllegalArgumentException if the arguments are incompatible + */ + public static void shallowCopyFieldState(final Object src, final Object dest) throws IllegalArgumentException { + if (src == null) { + throw new IllegalArgumentException("Source for field copy cannot be null"); + } + if (dest == null) { + throw new IllegalArgumentException("Destination for field copy cannot be null"); + } + if (!src.getClass().isAssignableFrom(dest.getClass())) { + throw new IllegalArgumentException("Destination class [" + dest.getClass().getName() + + "] must be same or subclass as source class [" + src.getClass().getName() + "]"); + } + doWithFields(src.getClass(), new FieldCallback() { + public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + makeAccessible(field); + Object srcValue = field.get(src); + field.set(dest, srcValue); + } + }, COPYABLE_FIELDS); + } + + + /** + * Action to take on each method. + */ + public interface MethodCallback { + + /** + * Perform an operation using the given method. + * @param method the method to operate on + */ + void doWith(Method method) throws IllegalArgumentException, IllegalAccessException; + } + + + /** + * Callback optionally used to method fields to be operated on by a method callback. + */ + public interface MethodFilter { + + /** + * Determine whether the given method matches. + * @param method the method to check + */ + boolean matches(Method method); + } + + /** + * Callback interface invoked on each field in the hierarchy. + */ + public interface FieldCallback { + + /** + * Perform an operation using the given field. + * @param field the field to operate on + */ + void doWith(Field field) throws IllegalArgumentException, IllegalAccessException; + } + + + /** + * Callback optionally used to filter fields to be operated on by a field callback. + */ + public interface FieldFilter { + + /** + * Determine whether the given field matches. + * @param field the field to check + */ + boolean matches(Field field); + } + + + /** + * Pre-built FieldFilter that matches all non-static, non-final fields. + */ + public static FieldFilter COPYABLE_FIELDS = new FieldFilter() { + + public boolean matches(Field field) { + return !(Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())); + } + }; + + /** + * Pre-built MethodFilter that matches all non-bridge methods. + */ + public static MethodFilter NON_BRIDGED_METHODS = new MethodFilter() { + + public boolean matches(Method method) { + return !method.isBridge(); + } + }; + +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ResourceUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ResourceUtils.java new file mode 100644 index 0000000..b8d3edf --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/ResourceUtils.java @@ -0,0 +1,351 @@ +package com.ksa.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +/** +* Utility methods for resolving resource locations to files in the +* file system. Mainly for internal use within the framework. +* +*

Consider using Spring's Resource abstraction in the core package +* for handling all kinds of file resources in a uniform manner. +* {@link org.springframework.core.io.ResourceLoader}'s getResource +* method can resolve any location to a {@link org.springframework.core.io.Resource} +* object, which in turn allows to obtain a java.io.File in the +* file system through its getFile() method. +* +*

The main reason for these utility methods for resource location handling +* is to support {@link Log4jConfigurer}, which must be able to resolve +* resource locations before the logging system has been initialized. +* Spring' Resource abstraction in the core package, on the other hand, +* already expects the logging system to be available. +* +* @author Juergen Hoeller +* @since 1.1.5 +* @see org.springframework.core.io.Resource +* @see org.springframework.core.io.ClassPathResource +* @see org.springframework.core.io.FileSystemResource +* @see org.springframework.core.io.UrlResource +* @see org.springframework.core.io.ResourceLoader +*/ +public abstract class ResourceUtils { + + /** Pseudo URL prefix for loading from the class path: "classpath:" */ + public static final String CLASSPATH_URL_PREFIX = "classpath:"; + + /** URL prefix for loading from the file system: "file:" */ + public static final String FILE_URL_PREFIX = "file:"; + + /** URL protocol for a file in the file system: "file" */ + public static final String URL_PROTOCOL_FILE = "file"; + + /** URL protocol for an entry from a jar file: "jar" */ + public static final String URL_PROTOCOL_JAR = "jar"; + + /** URL protocol for an entry from a zip file: "zip" */ + public static final String URL_PROTOCOL_ZIP = "zip"; + + /** URL protocol for an entry from a JBoss jar file: "vfszip" */ + public static final String URL_PROTOCOL_VFSZIP = "vfszip"; + + /** URL protocol for a JBoss VFS resource: "vfs" */ + public static final String URL_PROTOCOL_VFS = "vfs"; + + /** URL protocol for an entry from a WebSphere jar file: "wsjar" */ + public static final String URL_PROTOCOL_WSJAR = "wsjar"; + + /** URL protocol for an entry from an OC4J jar file: "code-source" */ + public static final String URL_PROTOCOL_CODE_SOURCE = "code-source"; + + /** Separator between JAR URL and file path within the JAR */ + public static final String JAR_URL_SEPARATOR = "!/"; + + + /** + * Return whether the given resource location is a URL: + * either a special "classpath" pseudo URL or a standard URL. + * @param resourceLocation the location String to check + * @return whether the location qualifies as a URL + * @see #CLASSPATH_URL_PREFIX + * @see java.net.URL + */ + public static boolean isUrl(String resourceLocation) { + if (resourceLocation == null) { + return false; + } + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + return true; + } + try { + new URL(resourceLocation); + return true; + } + catch (MalformedURLException ex) { + return false; + } + } + + /** + * Resolve the given resource location to a java.net.URL. + *

Does not check whether the URL actually exists; simply returns + * the URL that the given location would correspond to. + * @param resourceLocation the resource location to resolve: either a + * "classpath:" pseudo URL, a "file:" URL, or a plain file path + * @return a corresponding URL object + * @throws FileNotFoundException if the resource cannot be resolved to a URL + */ + public static URL getURL(String resourceLocation) throws FileNotFoundException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); + URL url = ClassUtils.getDefaultClassLoader().getResource(path); + if (url == null) { + String description = "class path resource [" + path + "]"; + throw new FileNotFoundException( + description + " cannot be resolved to URL because it does not exist"); + } + return url; + } + try { + // try URL + return new URL(resourceLocation); + } + catch (MalformedURLException ex) { + // no URL -> treat as file path + try { + return new File(resourceLocation).toURI().toURL(); + } + catch (MalformedURLException ex2) { + throw new FileNotFoundException("Resource location [" + resourceLocation + + "] is neither a URL not a well-formed file path"); + } + } + } + + /** + * Resolve the given resource location to a java.io.File, + * i.e. to a file in the file system. + *

Does not check whether the fil actually exists; simply returns + * the File that the given location would correspond to. + * @param resourceLocation the resource location to resolve: either a + * "classpath:" pseudo URL, a "file:" URL, or a plain file path + * @return a corresponding File object + * @throws FileNotFoundException if the resource cannot be resolved to + * a file in the file system + */ + public static File getFile(String resourceLocation) throws FileNotFoundException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); + String description = "class path resource [" + path + "]"; + URL url = ClassUtils.getDefaultClassLoader().getResource(path); + if (url == null) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system"); + } + return getFile(url, description); + } + try { + // try URL + return getFile(new URL(resourceLocation)); + } + catch (MalformedURLException ex) { + // no URL -> treat as file path + return new File(resourceLocation); + } + } + + /** + * Resolve the given resource URL to a java.io.File, + * i.e. to a file in the file system. + * @param resourceUrl the resource URL to resolve + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URL resourceUrl) throws FileNotFoundException { + return getFile(resourceUrl, "URL"); + } + + /** + * Resolve the given resource URL to a java.io.File, + * i.e. to a file in the file system. + * @param resourceUrl the resource URL to resolve + * @param description a description of the original resource that + * the URL was created for (for example, a class path location) + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URL resourceUrl, String description) throws FileNotFoundException { + Assert.notNull(resourceUrl, "Resource URL must not be null"); + if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system: " + resourceUrl); + } + try { + return new File(toURI(resourceUrl).getSchemeSpecificPart()); + } + catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + return new File(resourceUrl.getFile()); + } + } + + /** + * Resolve the given resource URI to a java.io.File, + * i.e. to a file in the file system. + * @param resourceUri the resource URI to resolve + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URI resourceUri) throws FileNotFoundException { + return getFile(resourceUri, "URI"); + } + + /** + * Resolve the given resource URI to a java.io.File, + * i.e. to a file in the file system. + * @param resourceUri the resource URI to resolve + * @param description a description of the original resource that + * the URI was created for (for example, a class path location) + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URI resourceUri, String description) throws FileNotFoundException { + Assert.notNull(resourceUri, "Resource URI must not be null"); + if (!URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system: " + resourceUri); + } + return new File(resourceUri.getSchemeSpecificPart()); + } + + /** + * Determine whether the given URL points to a resource in a jar file, + * that is, has protocol "jar", "zip", "wsjar" or "code-source". + *

"zip" and "wsjar" are used by BEA WebLogic Server and IBM WebSphere, respectively, + * but can be treated like jar files. The same applies to "code-source" URLs on Oracle + * OC4J, provided that the path contains a jar separator. + * @param url the URL to check + * @return whether the URL has been identified as a JAR URL + */ + public static boolean isJarURL(URL url) { + String protocol = url.getProtocol(); + return (URL_PROTOCOL_JAR.equals(protocol) || + URL_PROTOCOL_ZIP.equals(protocol) || + URL_PROTOCOL_WSJAR.equals(protocol) || + (URL_PROTOCOL_CODE_SOURCE.equals(protocol) && url.getPath().contains(JAR_URL_SEPARATOR))); + } + + /** + * Extract the URL for the actual jar file from the given URL + * (which may point to a resource in a jar file or to a jar file itself). + * @param jarUrl the original URL + * @return the URL for the actual jar file + * @throws MalformedURLException if no valid jar file URL could be extracted + */ + public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { + String urlFile = jarUrl.getFile(); + int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR); + if (separatorIndex != -1) { + String jarFile = urlFile.substring(0, separatorIndex); + try { + return new URL(jarFile); + } + catch (MalformedURLException ex) { + // Probably no protocol in original jar URL, like "jar:C:/mypath/myjar.jar". + // This usually indicates that the jar file resides in the file system. + if (!jarFile.startsWith("/")) { + jarFile = "/" + jarFile; + } + return new URL(FILE_URL_PREFIX + jarFile); + } + } + else { + return jarUrl; + } + } + + /** + * Create a URI instance for the given URL, + * replacing spaces with "%20" quotes first. + *

Furthermore, this method works on JDK 1.4 as well, + * in contrast to the URL.toURI() method. + * @param url the URL to convert into a URI instance + * @return the URI instance + * @throws URISyntaxException if the URL wasn't a valid URI + * @see java.net.URL#toURI() + */ + public static URI toURI(URL url) throws URISyntaxException { + return toURI(url.toString()); + } + + /** + * Create a URI instance for the given location String, + * replacing spaces with "%20" quotes first. + * @param location the location String to convert into a URI instance + * @return the URI instance + * @throws URISyntaxException if the location wasn't a valid URI + */ + public static URI toURI(String location) throws URISyntaxException { + return new URI(StringUtils.replace(location, " ", "%20")); + } + + + /* ------- 自定义的工具方法 ------------------- */ + + private static ResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver(); + + /** + * Return a Resource handle for the specified resource. + * The handle should always be a reusable resource descriptor, + * allowing for multiple {@link Resource#getInputStream()} calls. + *

    + *
  • Must support fully qualified URLs, e.g. "file:C:/test.dat". + *
  • Must support classpath pseudo-URLs, e.g. "classpath:test.dat". + * (This will be implementation-specific, typically provided by an + * ApplicationContext implementation.) + *
+ *

Note that a Resource handle does not imply an existing resource; + * you need to invoke {@link Resource#exists} to check for existence. + * @param location the resource location + * @return a corresponding Resource handle + * @see #CLASSPATH_URL_PREFIX + * @see org.springframework.core.io.Resource#exists + * @see org.springframework.core.io.Resource#getInputStream + */ + public static Resource getResource( String resource ) { + return resourceLoader.getResource( resource ); + } + + /** + * Resolve the given location pattern into Resource objects. + *

Overlapping resource entries that point to the same physical + * resource should be avoided, as far as possible. The result should + * have set semantics. + * @param locationPattern the location pattern to resolve + * @return the corresponding Resource objects + * @throws IOException in case of I/O errors + */ + public static Resource[] getResources( String resourcePattern ) throws IOException { + return resourceLoader.getResources( resourcePattern ); + } + + +} + diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/StringUtils.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/StringUtils.java new file mode 100644 index 0000000..1349aec --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/StringUtils.java @@ -0,0 +1,1100 @@ +package com.ksa.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeSet; + +/** + * Miscellaneous {@link String} utility methods. + * + *

Mainly for internal use within the framework; consider + * Jakarta's Commons Lang + * for a more comprehensive suite of String utilities. + * + *

This class delivers some simple functionality that should really + * be provided by the core Java String and {@link StringBuilder} + * classes, such as the ability to {@link #replace} all occurrences of a given + * substring in a target string. It also provides easy-to-use methods to convert + * between delimited strings, such as CSV strings, and collections and arrays. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Rick Evans + * @author Arjen Poutsma + * @since 16 April 2001 + * @see org.apache.commons.lang.StringUtils + */ +public abstract class StringUtils { + + private static final String FOLDER_SEPARATOR = "/"; + + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + + private static final String TOP_PATH = ".."; + + private static final String CURRENT_PATH = "."; + + private static final char EXTENSION_SEPARATOR = '.'; + + + //--------------------------------------------------------------------- + // General convenience methods for working with Strings + //--------------------------------------------------------------------- + + /** + * Check that the given CharSequence is neither null nor of length 0. + * Note: Will return true for a CharSequence that purely consists of whitespace. + *

+     * StringUtils.hasLength(null) = false
+     * StringUtils.hasLength("") = false
+     * StringUtils.hasLength(" ") = true
+     * StringUtils.hasLength("Hello") = true
+     * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null and has length + * @see #hasText(String) + */ + public static boolean hasLength(CharSequence str) { + return (str != null && str.length() > 0); + } + + /** + * Check that the given String is neither null nor of length 0. + * Note: Will return true for a String that purely consists of whitespace. + * @param str the String to check (may be null) + * @return true if the String is not null and has length + * @see #hasLength(CharSequence) + */ + public static boolean hasLength(String str) { + return hasLength((CharSequence) str); + } + + /** + * Check whether the given CharSequence has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + *

+     * StringUtils.hasText(null) = false
+     * StringUtils.hasText("") = false
+     * StringUtils.hasText(" ") = false
+     * StringUtils.hasText("12345") = true
+     * StringUtils.hasText(" 12345 ") = true
+     * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null, + * its length is greater than 0, and it does not contain whitespace only + * @see java.lang.Character#isWhitespace + */ + public static boolean hasText(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + * @param str the String to check (may be null) + * @return true if the String is not null, its length is + * greater than 0, and it does not contain whitespace only + * @see #hasText(CharSequence) + */ + public static boolean hasText(String str) { + return hasText((CharSequence) str); + } + + /** + * Check whether the given CharSequence contains any whitespace characters. + * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not empty and + * contains at least 1 whitespace character + * @see java.lang.Character#isWhitespace + */ + public static boolean containsWhitespace(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String contains any whitespace characters. + * @param str the String to check (may be null) + * @return true if the String is not empty and + * contains at least 1 whitespace character + * @see #containsWhitespace(CharSequence) + */ + public static boolean containsWhitespace(String str) { + return containsWhitespace((CharSequence) str); + } + + /** + * Trim leading and trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all whitespace from the given String: + * leading, trailing, and inbetween characters. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimAllWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + int index = 0; + while (sb.length() > index) { + if (Character.isWhitespace(sb.charAt(index))) { + sb.deleteCharAt(index); + } + else { + index++; + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimLeadingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimTrailingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied leading character from the given String. + * @param str the String to check + * @param leadingCharacter the leading character to be trimmed + * @return the trimmed String + */ + public static String trimLeadingCharacter(String str, char leadingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(0) == leadingCharacter) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied trailing character from the given String. + * @param str the String to check + * @param trailingCharacter the trailing character to be trimmed + * @return the trimmed String + */ + public static String trimTrailingCharacter(String str, char trailingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + + /** + * Test if the given String starts with the specified prefix, + * ignoring upper/lower case. + * @param str the String to check + * @param prefix the prefix to look for + * @see java.lang.String#startsWith + */ + public static boolean startsWithIgnoreCase(String str, String prefix) { + if (str == null || prefix == null) { + return false; + } + if (str.startsWith(prefix)) { + return true; + } + if (str.length() < prefix.length()) { + return false; + } + String lcStr = str.substring(0, prefix.length()).toLowerCase(); + String lcPrefix = prefix.toLowerCase(); + return lcStr.equals(lcPrefix); + } + + /** + * Test if the given String ends with the specified suffix, + * ignoring upper/lower case. + * @param str the String to check + * @param suffix the suffix to look for + * @see java.lang.String#endsWith + */ + public static boolean endsWithIgnoreCase(String str, String suffix) { + if (str == null || suffix == null) { + return false; + } + if (str.endsWith(suffix)) { + return true; + } + if (str.length() < suffix.length()) { + return false; + } + + String lcStr = str.substring(str.length() - suffix.length()).toLowerCase(); + String lcSuffix = suffix.toLowerCase(); + return lcStr.equals(lcSuffix); + } + + /** + * Test whether the given string matches the given substring + * at the given index. + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against + * @param substring the substring to match at the given index + */ + public static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Count the occurrences of the substring in string s. + * @param str string to search in. Return 0 if this is null. + * @param sub string to search for. Return 0 if this is null. + */ + public static int countOccurrencesOf(String str, String sub) { + if (str == null || sub == null || str.length() == 0 || sub.length() == 0) { + return 0; + } + int count = 0; + int pos = 0; + int idx; + while ((idx = str.indexOf(sub, pos)) != -1) { + ++count; + pos = idx + sub.length(); + } + return count; + } + + /** + * Replace all occurences of a substring within a string with + * another string. + * @param inString String to examine + * @param oldPattern String to replace + * @param newPattern String to insert + * @return a String with the replacements + */ + public static String replace(String inString, String oldPattern, String newPattern) { + if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { + return inString; + } + StringBuilder sb = new StringBuilder(); + int pos = 0; // our position in the old string + int index = inString.indexOf(oldPattern); + // the index of an occurrence we've found, or -1 + int patLen = oldPattern.length(); + while (index >= 0) { + sb.append(inString.substring(pos, index)); + sb.append(newPattern); + pos = index + patLen; + index = inString.indexOf(oldPattern, pos); + } + sb.append(inString.substring(pos)); + // remember to append any characters to the right of a match + return sb.toString(); + } + + /** + * Delete all occurrences of the given substring. + * @param inString the original String + * @param pattern the pattern to delete all occurrences of + * @return the resulting String + */ + public static String delete(String inString, String pattern) { + return replace(inString, pattern, ""); + } + + /** + * Delete any character in a given String. + * @param inString the original String + * @param charsToDelete a set of characters to delete. + * E.g. "az\n" will delete 'a's, 'z's and new lines. + * @return the resulting String + */ + public static String deleteAny(String inString, String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + sb.append(c); + } + } + return sb.toString(); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------------------- + + /** + * Quote the given String with single quotes. + * @param str the input String (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or null if the input was null + */ + public static String quote(String str) { + return (str != null ? "'" + str + "'" : null); + } + + /** + * Turn the given Object into a String with single quotes + * if it is a String; keeping the Object as-is else. + * @param obj the input Object (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or the input object as-is if not a String + */ + public static Object quoteIfString(Object obj) { + return (obj instanceof String ? quote((String) obj) : obj); + } + + /** + * Unqualify a string qualified by a '.' dot character. For example, + * "this.name.is.qualified", returns "qualified". + * @param qualifiedName the qualified name + */ + public static String unqualify(String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + /** + * Unqualify a string qualified by a separator character. For example, + * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * @param qualifiedName the qualified name + * @param separator the separator + */ + public static String unqualify(String qualifiedName, char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + /** + * Capitalize a String, changing the first letter to + * upper case as per {@link Character#toUpperCase(char)}. + * No other letters are changed. + * @param str the String to capitalize, may be null + * @return the capitalized String, null if null + */ + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * Uncapitalize a String, changing the first letter to + * lower case as per {@link Character#toLowerCase(char)}. + * No other letters are changed. + * @param str the String to uncapitalize, may be null + * @return the uncapitalized String, null if null + */ + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if (str == null || str.length() == 0) { + return str; + } + StringBuilder sb = new StringBuilder(str.length()); + if (capitalize) { + sb.append(Character.toUpperCase(str.charAt(0))); + } + else { + sb.append(Character.toLowerCase(str.charAt(0))); + } + sb.append(str.substring(1)); + return sb.toString(); + } + + /** + * Extract the filename from the given path, + * e.g. "mypath/myfile.txt" -> "myfile.txt". + * @param path the file path (may be null) + * @return the extracted filename, or null if none + */ + public static String getFilename(String path) { + if (path == null) { + return null; + } + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + /** + * Extract the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "txt". + * @param path the file path (may be null) + * @return the extracted filename extension, or null if none + */ + public static String getFilenameExtension(String path) { + if (path == null) { + return null; + } + int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + return (sepIndex != -1 ? path.substring(sepIndex + 1) : null); + } + + /** + * Strip the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * @param path the file path (may be null) + * @return the path with stripped filename extension, + * or null if none + */ + public static String stripFilenameExtension(String path) { + if (path == null) { + return null; + } + int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + return (sepIndex != -1 ? path.substring(0, sepIndex) : path); + } + + /** + * Apply the given relative path to the given path, + * assuming standard Java folder separation (i.e. "/" separators); + * @param path the path to start from (usually a full file path) + * @param relativePath the relative path to apply + * (relative to the full file path above) + * @return the full file path that results from applying the relative path + */ + public static String applyRelativePath(String path, String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (separatorIndex != -1) { + String newPath = path.substring(0, separatorIndex); + if (!relativePath.startsWith(FOLDER_SEPARATOR)) { + newPath += FOLDER_SEPARATOR; + } + return newPath + relativePath; + } + else { + return relativePath; + } + } + + /** + * Normalize the path by suppressing sequences like "path/.." and + * inner simple dots. + *

The result is convenient for path comparison. For other uses, + * notice that Windows separators ("\") are replaced by simple slashes. + * @param path the original path + * @return the normalized path + */ + public static String cleanPath(String path) { + if (path == null) { + return null; + } + String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + + // Strip prefix from path to analyze, to not treat it as part of the + // first path element. This is necessary to correctly parse paths like + // "file:core/../core/io/Resource.class", where the ".." should just + // strip the first "core" directory while keeping the "file:" prefix. + int prefixIndex = pathToUse.indexOf(":"); + String prefix = ""; + if (prefixIndex != -1) { + prefix = pathToUse.substring(0, prefixIndex + 1); + pathToUse = pathToUse.substring(prefixIndex + 1); + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR); + List pathElements = new LinkedList(); + int tops = 0; + + for (int i = pathArray.length - 1; i >= 0; i--) { + String element = pathArray[i]; + if (CURRENT_PATH.equals(element)) { + // Points to current directory - drop it. + } + else if (TOP_PATH.equals(element)) { + // Registering top path found. + tops++; + } + else { + if (tops > 0) { + // Merging path element with element corresponding to top path. + tops--; + } + else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + + // Remaining top paths need to be retained. + for (int i = 0; i < tops; i++) { + pathElements.add(0, TOP_PATH); + } + + return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + } + + /** + * Compare two paths after normalization of them. + * @param path1 first path for comparison + * @param path2 second path for comparison + * @return whether the two paths are equivalent after normalization + */ + public static boolean pathEquals(String path1, String path2) { + return cleanPath(path1).equals(cleanPath(path2)); + } + + /** + * Parse the given localeString into a {@link Locale}. + *

This is the inverse operation of {@link Locale#toString Locale's toString}. + * @param localeString the locale string, following Locale's + * toString() format ("en", "en_UK", etc); + * also accepts spaces as separators, as an alternative to underscores + * @return a corresponding Locale instance + */ + public static Locale parseLocaleString(String localeString) { + String[] parts = tokenizeToStringArray(localeString, "_ ", false, false); + String language = (parts.length > 0 ? parts[0] : ""); + String country = (parts.length > 1 ? parts[1] : ""); + String variant = ""; + if (parts.length >= 2) { + // There is definitely a variant, and it is everything after the country + // code sans the separator between the country code and the variant. + int endIndexOfCountryCode = localeString.indexOf(country) + country.length(); + // Strip off any leading '_' and whitespace, what's left is the variant. + variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode)); + if (variant.startsWith("_")) { + variant = trimLeadingCharacter(variant, '_'); + } + } + return (language.length() > 0 ? new Locale(language, country, variant) : null); + } + + /** + * Determine the RFC 3066 compliant language tag, + * as used for the HTTP "Accept-Language" header. + * @param locale the Locale to transform to a language tag + * @return the RFC 3066 compliant language tag as String + */ + public static String toLanguageTag(Locale locale) { + return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : ""); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with String arrays + //--------------------------------------------------------------------- + + /** + * Append the given String to the given String array, returning a new array + * consisting of the input array contents plus the given String. + * @param array the array to append to (can be null) + * @param str the String to append + * @return the new array (never null) + */ + public static String[] addStringToArray(String[] array, String str) { + if (ObjectUtils.isEmpty(array)) { + return new String[] {str}; + } + String[] newArr = new String[array.length + 1]; + System.arraycopy(array, 0, newArr, 0, array.length); + newArr[array.length] = str; + return newArr; + } + + /** + * Concatenate the given String arrays into one, + * with overlapping array elements included twice. + *

The order of elements in the original arrays is preserved. + * @param array1 the first array (can be null) + * @param array2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] concatenateStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + String[] newArr = new String[array1.length + array2.length]; + System.arraycopy(array1, 0, newArr, 0, array1.length); + System.arraycopy(array2, 0, newArr, array1.length, array2.length); + return newArr; + } + + /** + * Merge the given String arrays into one, with overlapping + * array elements only included once. + *

The order of elements in the original arrays is preserved + * (with the exception of overlapping elements, which are only + * included on their first occurence). + * @param array1 the first array (can be null) + * @param array2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] mergeStringArrays(String[] array1, String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + List result = new ArrayList(); + result.addAll(Arrays.asList(array1)); + for (String str : array2) { + if (!result.contains(str)) { + result.add(str); + } + } + return toStringArray(result); + } + + /** + * Turn given source String array into sorted array. + * @param array the source array + * @return the sorted array (never null) + */ + public static String[] sortStringArray(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + Arrays.sort(array); + return array; + } + + /** + * Copy the given Collection into a String array. + * The Collection must contain String elements only. + * @param collection the Collection to copy + * @return the String array (null if the passed-in + * Collection was null) + */ + public static String[] toStringArray(Collection collection) { + if (collection == null) { + return null; + } + return collection.toArray(new String[collection.size()]); + } + + /** + * Copy the given Enumeration into a String array. + * The Enumeration must contain String elements only. + * @param enumeration the Enumeration to copy + * @return the String array (null if the passed-in + * Enumeration was null) + */ + public static String[] toStringArray(Enumeration enumeration) { + if (enumeration == null) { + return null; + } + List list = Collections.list(enumeration); + return list.toArray(new String[list.size()]); + } + + /** + * Trim the elements of the given String array, + * calling String.trim() on each of them. + * @param array the original String array + * @return the resulting array (of the same size) with trimmed elements + */ + public static String[] trimArrayElements(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return new String[0]; + } + String[] result = new String[array.length]; + for (int i = 0; i < array.length; i++) { + String element = array[i]; + result[i] = (element != null ? element.trim() : null); + } + return result; + } + + /** + * Remove duplicate Strings from the given array. + * Also sorts the array, as it uses a TreeSet. + * @param array the String array + * @return an array without duplicates, in natural sort order + */ + public static String[] removeDuplicateStrings(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return array; + } + Set set = new TreeSet(); + for (String element : array) { + set.add(element); + } + return toStringArray(set); + } + + /** + * Split a String at the first occurrence of the delimiter. + * Does not include the delimiter in the result. + * @param toSplit the string to split + * @param delimiter to split the string up with + * @return a two element array with index 0 being before the delimiter, and + * index 1 being after the delimiter (neither element includes the delimiter); + * or null if the delimiter wasn't found in the given input String + */ + public static String[] split(String toSplit, String delimiter) { + if (!hasLength(toSplit) || !hasLength(delimiter)) { + return null; + } + int offset = toSplit.indexOf(delimiter); + if (offset < 0) { + return null; + } + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delimiter.length()); + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + return splitArrayElementsIntoProperties(array, delimiter, null); + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @param charsToDelete one or more characters to remove from each element + * prior to attempting the split operation (typically the quotation mark + * symbol), or null if no removal should occur + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties( + String[] array, String delimiter, String charsToDelete) { + + if (ObjectUtils.isEmpty(array)) { + return null; + } + Properties result = new Properties(); + for (String element : array) { + if (charsToDelete != null) { + element = deleteAny(element, charsToDelete); + } + String[] splittedElement = split(element, delimiter); + if (splittedElement == null) { + continue; + } + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + return result; + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + * Trims tokens and omits empty tokens. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter). + * @return an array of the tokens + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter) + * @param trimTokens trim the tokens via String's trim + * @param ignoreEmptyTokens omit empty tokens from the result array + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). + * @return an array of the tokens (null if the input String + * was null) + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray( + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter) { + return delimitedListToStringArray(str, delimiter, null); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @param charsToDelete a set of characters to delete. Useful for deleting unwanted + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) { + if (str == null) { + return new String[0]; + } + if (delimiter == null) { + return new String[] {str}; + } + List result = new ArrayList(); + if ("".equals(delimiter)) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } + else { + int pos = 0; + int delPos; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return toStringArray(result); + } + + /** + * Convert a CSV list into an array of Strings. + * @param str the input String + * @return an array of Strings, or the empty array in case of empty input + */ + public static String[] commaDelimitedListToStringArray(String str) { + return delimitedListToStringArray(str, ","); + } + + /** + * Convenience method to convert a CSV string list to a set. + * Note that this will suppress duplicates. + * @param str the input String + * @return a Set of String entries in the list + */ + public static Set commaDelimitedListToSet(String str) { + Set set = new TreeSet(); + String[] tokens = commaDelimitedListToStringArray(str); + for (String token : tokens) { + set.add(token); + } + return set; + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @param prefix the String to start each element with + * @param suffix the String to end each element with + * @return the delimited String + */ + public static String collectionToDelimitedString(Collection coll, String delim, String prefix, String suffix) { + if (CollectionUtils.isEmpty(coll)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + Iterator it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext()) { + sb.append(delim); + } + } + return sb.toString(); + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String collectionToDelimitedString(Collection coll, String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + /** + * Convenience method to return a Collection as a CSV String. + * E.g. useful for toString() implementations. + * @param coll the Collection to display + * @return the delimited String + */ + public static String collectionToCommaDelimitedString(Collection coll) { + return collectionToDelimitedString(coll, ","); + } + + /** + * Convenience method to return a String array as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param arr the array to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String arrayToDelimitedString(Object[] arr, String delim) { + if (ObjectUtils.isEmpty(arr)) { + return ""; + } + if (arr.length == 1) { + return ObjectUtils.nullSafeToString(arr[0]); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + sb.append(delim); + } + sb.append(arr[i]); + } + return sb.toString(); + } + + /** + * Convenience method to return a String array as a CSV String. + * E.g. useful for toString() implementations. + * @param arr the array to display + * @return the delimited String + */ + public static String arrayToCommaDelimitedString(Object[] arr) { + return arrayToDelimitedString(arr, ","); + } + +} \ No newline at end of file diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/Base64.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/Base64.java new file mode 100644 index 0000000..bc4b2f7 --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/Base64.java @@ -0,0 +1,1170 @@ +package com.ksa.util.codec; + +import java.math.BigInteger; + +import com.ksa.util.codec.CharEncoding; +import java.io.UnsupportedEncodingException; + +public class Base64 { + + public static final String DEFAULT_TEXT_ENCODING = "UTF-8"; + /** + * MIME chunk size per RFC 2045 section 6.8. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts + * all other characters, including any equal signs. + *

+ * + * @see RFC 2045 section + * 6.8 + */ + public static final int MIME_CHUNK_SIZE = 76; + + /** + * PEM chunk size per RFC 1421 section 4.3.2.4. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts + * all other characters, including any equal signs. + *

+ * + * @see RFC 1421 section + * 4.3.2.4 + */ + public static final int PEM_CHUNK_SIZE = 64; + + private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2; + + /** + * Defines the default buffer size - currently {@value} - must be large + * enough for at least one encoded block+separator + */ + private static final int DEFAULT_BUFFER_SIZE = 8192; + + /** Mask used to extract 8 bits, used in decoding bytes */ + protected static final int MASK_8BITS = 0xff; + + /** + * Byte used to pad output. + */ + protected static final byte PAD_DEFAULT = '='; // Allow static access to + // default + + protected final byte PAD = PAD_DEFAULT; // instance variable just in case it + // needs to vary later + + /** + * Chunksize for encoding. Not used when decoding. A value of zero or less + * implies no chunking of the encoded data. Rounded down to nearest multiple + * of encodedBlockSize. + */ + protected final int lineLength; + + /** + * Size of chunk separator. Not used unless {@link #lineLength} > 0. + */ + private final int chunkSeparatorLength; + + /** + * Buffer for streaming. + */ + protected byte[] buffer; + + /** + * Position where next character should be written in the buffer. + */ + protected int pos; + + /** + * Position where next character should be read from the buffer. + */ + private int readPos; + + /** + * Boolean flag to indicate the EOF has been reached. Once EOF has been + * reached, this object becomes useless, and must be thrown away. + */ + protected boolean eof; + + /** + * Variable tracks how many characters have been written to the current + * line. Only used when encoding. We use it to make sure each encoded line + * never goes beyond lineLength (if lineLength > 0). + */ + protected int currentLinePos; + + /** + * Writes to the buffer only occur after every 3/5 reads when encoding, and + * every 4/8 reads when decoding. This variable helps track that. + */ + protected int modulus; + + /** + * BASE32 characters are 6 bits in length. They are formed by taking a block + * of 3 octets to form a 24-bit string, which is converted into 4 BASE64 + * characters. + */ + private static final int BITS_PER_ENCODED_BYTE = 6; + private static final int BYTES_PER_UNENCODED_BLOCK = 3; + private static final int BYTES_PER_ENCODED_BLOCK = 4; + + /** + * Chunk separator per RFC 2045 section 2.1. + * + *

+ * N.B. The next major release may break compatibility and make this field + * private. + *

+ * + * @see RFC 2045 section + * 2.1 + */ + static final byte[] CHUNK_SEPARATOR = { '\r', '\n' }; + + /** + * This array is a lookup table that translates 6-bit positive integer index + * values into their "Base64 Alphabet" equivalents as specified in Table 1 + * of RFC 2045. + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] STANDARD_ENCODE_TABLE = { 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', + 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/' }; + + /** + * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and / + * changed to - and _ to make the encoded Base64 results more URL-SAFE. This + * table is only used when the Base64's mode is set to URL-SAFE. + */ + private static final byte[] URL_SAFE_ENCODE_TABLE = { 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', + 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '-', '_' }; + + /** + * This array is a lookup table that translates Unicode characters drawn + * from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into + * their 6-bit positive integer equivalents. Characters that are not in the + * Base64 alphabet but fall within the bounds of the array are translated to + * -1. + * + * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This + * means decoder seamlessly handles both URL_SAFE and STANDARD base64. (The + * encoder, on the other hand, needs to know ahead of time what to emit). + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] DECODE_TABLE = { -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, + -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; + + /** + * Base64 uses 6-bit fields. + */ + /** Mask used to extract 6 bits, used when encoding */ + private static final int MASK_6BITS = 0x3f; + + // The static final fields above are used for the original static byte[] + // methods on Base64. + // The private member fields below are used with the new streaming approach, + // which requires + // some state be preserved between calls of encode() and decode(). + + /** + * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE + * above remains static because it is able to decode both STANDARD and + * URL_SAFE streams, but the encodeTable must be a member variable so we can + * switch between the two modes. + */ + private final byte[] encodeTable; + + // Only one decode table currently; keep for consistency with Base32 code + private final byte[] decodeTable = DECODE_TABLE; + + /** + * Line separator for encoding. Not used when decoding. Only used if + * lineLength > 0. + */ + private final byte[] lineSeparator; + + /** + * Convenience variable to help us determine when our buffer is going to run + * out of room and needs resizing. + * decodeSize = 3 + lineSeparator.length; + */ + private final int decodeSize; + + /** + * Convenience variable to help us determine when our buffer is going to run + * out of room and needs resizing. + * encodeSize = 4 + lineSeparator.length; + */ + private final int encodeSize; + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this + * variable. + */ + private int bitWorkArea; + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in + * URL-unsafe mode. + *

+ * When encoding the line length is 0 (no chunking), and the encoding table + * is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ */ + public Base64() { + this(0); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in the + * given URL-safe mode. + *

+ * When encoding the line length is 76, the line separator is CRLF, and the + * encoding table is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ * + * @param urlSafe + * if true, URL-safe encoding is used. In most cases + * this should be set to false. + * @since 1.4 + */ + public Base64(boolean urlSafe) { + this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in + * URL-unsafe mode. + *

+ * When encoding the line length is given in the constructor, the line + * separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up + * being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length + * (rounded down to nearest multiple of 4). If lineLength <= 0, + * then the output will not be divided into lines (chunks). + * Ignored when decoding. + * @since 1.4 + */ + public Base64(int lineLength) { + this(lineLength, CHUNK_SEPARATOR); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in + * URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the + * constructor, and the encoding table is STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up + * being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length + * (rounded down to nearest multiple of 4). If lineLength <= 0, + * then the output will not be divided into lines (chunks). + * Ignored when decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of + * bytes. + * @throws IllegalArgumentException + * Thrown when the provided lineSeparator included some base64 + * characters. + * @since 1.4 + */ + public Base64(int lineLength, byte[] lineSeparator) { + this(lineLength, lineSeparator, false); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in + * URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the + * constructor, and the encoding table is STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up + * being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length + * (rounded down to nearest multiple of 4). If lineLength <= 0, + * then the output will not be divided into lines (chunks). + * Ignored when decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of + * bytes. + * @param urlSafe + * Instead of emitting '+' and '/' we emit '-' and '_' + * respectively. urlSafe is only applied to encode operations. + * Decoding seamlessly handles both modes. + * @throws IllegalArgumentException + * The provided lineSeparator included some base64 characters. + * That's not going to work! + */ + public Base64(int lineLength, byte[] lineSeparator, boolean urlSafe) { + this.chunkSeparatorLength = lineSeparator == null ? 0 + : lineSeparator.length; + this.lineLength = (lineLength > 0 && chunkSeparatorLength > 0) ? (lineLength / BYTES_PER_ENCODED_BLOCK) + * BYTES_PER_ENCODED_BLOCK + : 0; + + // @see test case Base64Test.testConstructors() + if (lineSeparator != null) { + if (containsAlphabetOrPad(lineSeparator)) { + String sep = convertToString(lineSeparator); + throw new IllegalArgumentException( + "lineSeparator must not contain base64 characters: [" + + sep + "]"); + } + if (lineLength > 0) { // null line-sep forces no chunking rather + // than throwing IAE + this.encodeSize = BYTES_PER_ENCODED_BLOCK + + lineSeparator.length; + this.lineSeparator = new byte[lineSeparator.length]; + System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, + lineSeparator.length); + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + this.decodeSize = this.encodeSize - 1; + this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE + : STANDARD_ENCODE_TABLE; + } + + /** + * Returns true if this object has buffered data for reading. + * + * @return true if there is data still available for reading. + */ + boolean hasData() { // package protected for access from I/O streams + return this.buffer != null; + } + + /** + * Returns the amount of buffered data available for reading. + * + * @return The amount of buffered data available for reading. + */ + int available() { // package protected for access from I/O streams + return buffer != null ? pos - readPos : 0; + } + + /** + * Get the default buffer size. Can be overridden. + * + * @return {@link #DEFAULT_BUFFER_SIZE} + */ + protected int getDefaultBufferSize() { + return DEFAULT_BUFFER_SIZE; + } + + /** Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}. */ + private void resizeBuffer() { + if (buffer == null) { + buffer = new byte[getDefaultBufferSize()]; + pos = 0; + readPos = 0; + } else { + byte[] b = new byte[buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR]; + System.arraycopy(buffer, 0, b, 0, buffer.length); + buffer = b; + } + } + + /** + * Ensure that the buffer has room for size bytes + * + * @param size + * minimum spare space required + */ + protected void ensureBufferSize(int size) { + if ((buffer == null) || (buffer.length < pos + size)) { + resizeBuffer(); + } + } + + /** + * Extracts buffered data into the provided byte[] array, starting at + * position bPos, up to a maximum of bAvail bytes. Returns how many bytes + * were actually extracted. + * + * @param b + * byte[] array to extract the buffered data into. + * @param bPos + * position in byte[] array to start extraction at. + * @param bAvail + * amount of bytes we're allowed to extract. We may extract fewer + * (if fewer are available). + * @return The number of bytes successfully extracted into the provided + * byte[] array. + */ + int readResults(byte[] b, int bPos, int bAvail) { // package protected for + // access from I/O + // streams + if (buffer != null) { + int len = Math.min(available(), bAvail); + System.arraycopy(buffer, readPos, b, bPos, len); + readPos += len; + if (readPos >= pos) { + buffer = null; // so hasData() will return false, and this + // method can return -1 + } + return len; + } + return eof ? -1 : 0; + } + + /** + * Checks if a byte value is whitespace or not. Whitespace is taken to mean: + * space, tab, CR, LF + * + * @param byteToCheck + * the byte to check + * @return true if byte is whitespace, false otherwise + */ + protected static boolean isWhiteSpace(byte byteToCheck) { + switch (byteToCheck) { + case ' ': + case '\n': + case '\r': + case '\t': + return true; + default: + return false; + } + } + + /** + * Resets this object to its initial newly constructed state. + */ + private void reset() { + buffer = null; + pos = 0; + readPos = 0; + currentLinePos = 0; + modulus = 0; + eof = false; + } + + /** + * Decodes a byte[] containing characters in the Base-N alphabet. + * + * @param pArray + * A byte array containing Base-N character data + * @return a byte array containing binary data + */ + public byte[] doDecode(byte[] pArray) { + reset(); + if (pArray == null || pArray.length == 0) { + return pArray; + } + decode(pArray, 0, pArray.length); + decode(pArray, 0, -1); // Notify decoder of EOF. + byte[] result = new byte[pos]; + readResults(result, 0, result.length); + return result; + } + + /** + * Encodes a byte[] containing binary data, into a byte[] containing + * characters in the alphabet. + * + * @param pArray + * a byte array containing binary data + * @return A byte array containing only the basen alphabetic character data + */ + public byte[] doEncode(byte[] pArray) { + reset(); + if (pArray == null || pArray.length == 0) { + return pArray; + } + encode(pArray, 0, pArray.length); + encode(pArray, 0, -1); // Notify encoder of EOF. + byte[] buf = new byte[pos - readPos]; + readResults(buf, 0, buf.length); + return buf; + } + + /** + *

+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. + * Must be called at least twice: once with the data to encode, and once + * with inAvail set to "-1" to alert encoder that EOF has been reached, so + * flush last remaining bytes (if not multiple of 3). + *

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, + * and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in + * byte[] array of binary data to base64 encode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + */ + void encode(byte[] in, int inPos, int inAvail) { + if (eof) { + return; + } + // inAvail < 0 is how we're informed of EOF in the underlying data we're + // encoding. + if (inAvail < 0) { + eof = true; + if (0 == modulus && lineLength == 0) { + return; // no leftovers to process and not using chunking + } + ensureBufferSize(encodeSize); + int savedPos = pos; + switch (modulus) { // 0-2 + case 1: // 8 bits = 6 + 2 + buffer[pos++] = encodeTable[(bitWorkArea >> 2) & MASK_6BITS]; // top + // 6 + // bits + buffer[pos++] = encodeTable[(bitWorkArea << 4) & MASK_6BITS]; // remaining + // 2 + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[pos++] = PAD; + buffer[pos++] = PAD; + } + break; + + case 2: // 16 bits = 6 + 6 + 4 + buffer[pos++] = encodeTable[(bitWorkArea >> 10) & MASK_6BITS]; + buffer[pos++] = encodeTable[(bitWorkArea >> 4) & MASK_6BITS]; + buffer[pos++] = encodeTable[(bitWorkArea << 2) & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[pos++] = PAD; + } + break; + } + currentLinePos += pos - savedPos; // keep track of current line + // position + // if currentPos == 0 we are at the start of a line, so don't add + // CRLF + if (lineLength > 0 && currentLinePos > 0) { + System.arraycopy(lineSeparator, 0, buffer, pos, + lineSeparator.length); + pos += lineSeparator.length; + } + } else { + for (int i = 0; i < inAvail; i++) { + ensureBufferSize(encodeSize); + modulus = (modulus + 1) % BYTES_PER_UNENCODED_BLOCK; + int b = in[inPos++]; + if (b < 0) { + b += 256; + } + bitWorkArea = (bitWorkArea << 8) + b; // BITS_PER_BYTE + if (0 == modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract + buffer[pos++] = encodeTable[(bitWorkArea >> 18) + & MASK_6BITS]; + buffer[pos++] = encodeTable[(bitWorkArea >> 12) + & MASK_6BITS]; + buffer[pos++] = encodeTable[(bitWorkArea >> 6) & MASK_6BITS]; + buffer[pos++] = encodeTable[bitWorkArea & MASK_6BITS]; + currentLinePos += BYTES_PER_ENCODED_BLOCK; + if (lineLength > 0 && lineLength <= currentLinePos) { + System.arraycopy(lineSeparator, 0, buffer, pos, + lineSeparator.length); + pos += lineSeparator.length; + currentLinePos = 0; + } + } + } + } + } + + /** + *

+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. + * Should be called at least twice: once with the data to decode, and once + * with inAvail set to "-1" to alert decoder that EOF has been reached. The + * "-1" call is not necessary when decoding, but it doesn't hurt, either. + *

+ *

+ * Ignores all non-base64 characters. This is how chunked (e.g. 76 + * character) data is handled, since CR and LF are silently ignored, but has + * implications for other bytes, too. This method subscribes to the + * garbage-in, garbage-out philosophy: it will not check the provided data + * for validity. + *

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, + * and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in + * byte[] array of ascii data to base64 decode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + */ + void decode(byte[] in, int inPos, int inAvail) { + if (eof) { + return; + } + if (inAvail < 0) { + eof = true; + } + for (int i = 0; i < inAvail; i++) { + ensureBufferSize(decodeSize); + byte b = in[inPos++]; + if (b == PAD) { + // We're done. + eof = true; + break; + } else { + if (b >= 0 && b < DECODE_TABLE.length) { + int result = DECODE_TABLE[b]; + if (result >= 0) { + modulus = (modulus + 1) % BYTES_PER_ENCODED_BLOCK; + bitWorkArea = (bitWorkArea << BITS_PER_ENCODED_BYTE) + + result; + if (modulus == 0) { + buffer[pos++] = (byte) ((bitWorkArea >> 16) & MASK_8BITS); + buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS); + buffer[pos++] = (byte) (bitWorkArea & MASK_8BITS); + } + } + } + } + } + + // Two forms of EOF as far as base64 decoder is concerned: actual + // EOF (-1) and first time '=' character is encountered in stream. + // This approach makes the '=' padding characters completely optional. + if (eof && modulus != 0) { + ensureBufferSize(decodeSize); + + // We have some spare bits remaining + // Output all whole multiples of 8 bits and ignore the rest + switch (modulus) { + // case 1: // 6 bits - ignore entirely + // break; + case 2: // 12 bits = 8 + 4 + bitWorkArea = bitWorkArea >> 4; // dump the extra 4 bits + buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS); + break; + case 3: // 18 bits = 8 + 8 + 2 + bitWorkArea = bitWorkArea >> 2; // dump 2 bits + buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS); + buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS); + break; + } + } + } + + /** + * Returns whether or not the octet is in the current alphabet. + * Does not allow whitespace or pad. + * + * @param value + * The value to test + * + * @return true if the value is defined in the current + * alphabet, false otherwise. + */ + // protected abstract boolean isInAlphabet(byte value); + + /** + * Tests a given byte array to see if it contains only valid characters + * within the alphabet. The method optionally treats whitespace and pad as + * valid. + * + * @param arrayOctet + * byte array to test + * @param allowWSPad + * if true, then whitespace and PAD are also allowed + * + * @return true if all bytes are valid characters in the + * alphabet or if the byte array is empty; false, + * otherwise + */ + public boolean isInAlphabet(byte[] arrayOctet, boolean allowWSPad) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isInAlphabet(arrayOctet[i]) + && (!allowWSPad || (arrayOctet[i] != PAD) + && !isWhiteSpace(arrayOctet[i]))) { + return false; + } + } + return true; + } + + /** + * Tests a given String to see if it contains only valid characters within + * the alphabet. The method treats whitespace and PAD as valid. + * + * @param basen + * String to test + * @return true if all characters in the String are valid + * characters in the alphabet or if the String is empty; + * false, otherwise + * @see #isInAlphabet(byte[], boolean) + */ + public boolean isInAlphabet(String basen) { + return isInAlphabet(convertToBytes(basen), true); + } + + /** + * Tests a given byte array to see if it contains any characters within the + * alphabet or PAD. + * + * Intended for use in checking line-ending arrays + * + * @param arrayOctet + * byte array to test + * @return true if any byte is a valid character in the + * alphabet or PAD; false otherwise + */ + protected boolean containsAlphabetOrPad(byte[] arrayOctet) { + if (arrayOctet == null) { + return false; + } + for (int i = 0; i < arrayOctet.length; i++) { + if (PAD == arrayOctet[i] || isInAlphabet(arrayOctet[i])) { + return true; + } + } + return false; + } + + /** + * Calculates the amount of space needed to encode the supplied array. + * + * @param pArray + * byte[] array which will later be encoded + * + * @return amount of space needed to encoded the supplied array. Returns a + * long since a max-len array will require > Integer.MAX_VALUE + */ + public long getEncodedLength(byte[] pArray) { + // Calculate non-chunked size - rounded up to allow for padding + // cast to long is needed to avoid possibility of overflow + long len = ((pArray.length + BYTES_PER_UNENCODED_BLOCK - 1) / BYTES_PER_UNENCODED_BLOCK) + * (long) BYTES_PER_ENCODED_BLOCK; + if (lineLength > 0) { // We're using chunking + // Round up to nearest multiple + len += ((len + lineLength - 1) / lineLength) * chunkSeparatorLength; + } + return len; + } + + + /** + * Returns whether or not the octet is in the Base32 alphabet. + * + * @param octet + * The value to test + * @return true if the value is defined in the the Base32 + * alphabet false otherwise. + */ + protected boolean isInAlphabet(byte octet) { + return octet >= 0 && octet < decodeTable.length + && decodeTable[octet] != -1; + } + + /** + * Returns whether or not the octet is in the base 64 alphabet. + * + * @param octet + * The value to test + * @return true if the value is defined in the the base 64 + * alphabet, false otherwise. + * @since 1.4 + */ + public static boolean isBase64(byte octet) { + return octet == PAD_DEFAULT + || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1); + } + + /** + * Tests a given String to see if it contains only valid characters within + * the Base64 alphabet. Currently the method treats whitespace as valid. + * + * @param base64 + * String to test + * @return true if all characters in the String are valid + * characters in the Base64 alphabet or if the String is empty; + * false, otherwise + * @since 1.5 + */ + public static boolean isBase64(String base64) { + return isBase64(convertToBytes(base64)); + } + + /** + * Tests a given byte array to see if it contains only valid characters + * within the Base64 alphabet. Currently the method treats whitespace as + * valid. + * + * @param arrayOctet + * byte array to test + * @return true if all bytes are valid characters in the Base64 + * alphabet or if the byte array is empty; false, + * otherwise + * @since 1.5 + */ + public static boolean isBase64(byte[] arrayOctet) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) { + return false; + } + } + return true; + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the + * output. + * + * @param binaryData + * binary data to encode + * @return byte[] containing Base64 characters in their UTF-8 + * representation. + */ + public static byte[] encode(byte[] binaryData) { + return encode(binaryData, false); + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the + * output. + * + * @param str + * String to encode + * @return String in UTF-8 representation. + */ + public static String encode(String str) { + byte[] bytes = convertToBytes(str); + return convertToString(encode(bytes, false)); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the + * output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output + * into 76 character blocks + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than + * {@link Integer#MAX_VALUE} + */ + public static byte[] encode(byte[] binaryData, boolean isChunked) { + return encode(binaryData, isChunked, false); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the + * output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output + * into 76 character blocks + * @param urlSafe + * if true this encoder will emit - and _ instead of + * the usual + and / characters. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than + * {@link Integer#MAX_VALUE} + * @since 1.4 + */ + public static byte[] encode(byte[] binaryData, boolean isChunked, + boolean urlSafe) { + return encode(binaryData, isChunked, urlSafe, Integer.MAX_VALUE); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the + * output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output + * into 76 character blocks + * @param urlSafe + * if true this encoder will emit - and _ instead of + * the usual + and / characters. + * @param maxResultSize + * The maximum result size to accept. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than + * maxResultSize + * @since 1.4 + */ + public static byte[] encode(byte[] binaryData, boolean isChunked, + boolean urlSafe, int maxResultSize) { + if (binaryData == null || binaryData.length == 0) { + return binaryData; + } + + // Create this so can use the super-class method + // Also ensures that the same roundings are performed by the ctor and + // the code + Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, + CHUNK_SEPARATOR, urlSafe); + long len = b64.getEncodedLength(binaryData); + if (len > maxResultSize) { + throw new IllegalArgumentException( + "Input array too big, the output array would be bigger (" + + len + ") than the specified maximum size of " + + maxResultSize); + } + + return b64.doEncode(binaryData); + } + + /** + * Decodes a Base64 String. + * + * @param base64String + * String containing Base64 data + * @return Decoded String. + */ + public static String decode(String base64String) { + byte[] bytes = convertToBytes(base64String); + return convertToString(decode(bytes)); + } + + /** + * Decodes Base64 data into octets + * + * @param base64Data + * Byte array containing Base64 data + * @return Array containing decoded data. + */ + public static byte[] decode(byte[] base64Data) { + return new Base64().doDecode(base64Data); + } + + // Implementation of the Encoder Interface + + // Implementation of integer encoding used for crypto + /** + * Decodes a byte64-encoded integer according to crypto standards such as + * W3C's XML-Signature + * + * @param pArray + * a byte array containing base64 character data + * @return A BigInteger + * @since 1.4 + */ + public static BigInteger decodeInteger(byte[] pArray) { + return new BigInteger(1, decode(pArray)); + } + + /** + * Encodes to a byte64-encoded integer according to crypto standards such as + * W3C's XML-Signature + * + * @param bigInt + * a BigInteger + * @return A byte array containing base64 character data + * @throws NullPointerException + * if null is passed in + * @since 1.4 + */ + public static byte[] encodeInteger(BigInteger bigInt) { + if (bigInt == null) { + throw new NullPointerException( + "encodeInteger called with null parameter"); + } + return encode(toIntegerBytes(bigInt), false); + } + + /** + * Returns a byte-array representation of a BigInteger without + * sign bit. + * + * @param bigInt + * BigInteger to be converted + * @return a byte array representation of the BigInteger parameter + */ + static byte[] toIntegerBytes(BigInteger bigInt) { + int bitlen = bigInt.bitLength(); + // round bitlen + bitlen = ((bitlen + 7) >> 3) << 3; + byte[] bigBytes = bigInt.toByteArray(); + + if (((bigInt.bitLength() % 8) != 0) + && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bigInt.bitLength() % 8) == 0) { + startSrc = 1; + len--; + } + int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec + byte[] resizedBytes = new byte[bitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + + + private static IllegalStateException newIllegalStateException(String charsetName, UnsupportedEncodingException e) { + return new IllegalStateException(charsetName + ": " + e); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the given charset. + *

+ * This method catches {@link UnsupportedEncodingException} and re-throws it as {@link IllegalStateException}, which + * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE. + *

+ * + * @param bytes + * The bytes to be decoded into characters, may be null + * @param charsetName + * The name of a required {@link java.nio.charset.Charset} + * @return A new String decoded from the specified array of bytes using the given charset, + * or null if the input byte arrray was null. + * @throws IllegalStateException + * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a + * required charset name. + * @see CharEncoding + * @see String#String(byte[], String) + */ + public static String convertToString(byte[] bytes, String charsetName) { + if (bytes == null) { + return null; + } + try { + return new String(bytes, charsetName); + } catch (UnsupportedEncodingException e) { + throw newIllegalStateException(charsetName, e); + } + } + + /** + * Encodes the given string into a sequence of bytes using the named charset, storing the result into a new byte + * array. + *

+ * This method catches {@link UnsupportedEncodingException} and rethrows it as {@link IllegalStateException}, which + * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE. + *

+ * + * @param string + * the String to encode, may be null + * @param charsetName + * The name of a required {@link java.nio.charset.Charset} + * @return encoded bytes, or null if the input string was null + * @throws IllegalStateException + * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a + * required charset name. + * @see CharEncoding + * @see String#getBytes(String) + */ + public static byte[] convertToBytes(String string, String charsetName) { + if (string == null) { + return null; + } + try { + return string.getBytes(charsetName); + } catch (UnsupportedEncodingException e) { + throw newIllegalStateException(charsetName, e); + } + } + + /** + * Constructs a new String by decoding the specified array of bytes using UTF-8 charset. + * + * @param bytes + * The bytes to be decoded into characters, may be null + * + * @return A new String decoded from the specified array of bytes using the given charset, + * or null if the input byte arrray was null. + * + * @see CharEncoding + * @see String#String(byte[], String) + */ + public static String convertToString(byte[] bytes) { + return convertToString(bytes, CharEncoding.UTF_8); + } + + /** + * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @see CharEncoding + * @see String#getBytes(String) + */ + public static byte[] convertToBytes(String string) { + return convertToBytes(string, CharEncoding.UTF_8); + } +} diff --git a/test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/CharEncoding.java b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/CharEncoding.java new file mode 100644 index 0000000..381752b --- /dev/null +++ b/test_input/ksa/ksa-core/src/main/java/com/ksa/util/codec/CharEncoding.java @@ -0,0 +1,109 @@ +package com.ksa.util.codec; + +/** + * Character encoding names required of every implementation of the Java platform. + * + * From the Java documentation Standard + * charsets: + *

+ * Every implementation of the Java platform is required to support the following character encodings. Consult the + * release documentation for your implementation to see if any other encodings are supported. Consult the release + * documentation for your implementation to see if any other encodings are supported. + *

+ * + *
    + *
  • US-ASCII
    + * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.
  • + *
  • ISO-8859-1
    + * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.
  • + *
  • UTF-8
    + * Eight-bit Unicode Transformation Format.
  • + *
  • UTF-16BE
    + * Sixteen-bit Unicode Transformation Format, big-endian byte order.
  • + *
  • UTF-16LE
    + * Sixteen-bit Unicode Transformation Format, little-endian byte order.
  • + *
  • UTF-16
    + * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order + * accepted on input, big-endian used on output.)
  • + *
+ * + * This perhaps would best belong in the [lang] project. Even if a similar interface is defined in [lang], it is not + * forseen that [codec] would be made to depend on [lang]. + * + * @see Standard charsets + * @author Apache Software Foundation + * @since 1.4 + * @version $Id: CharEncoding.java 797857 2009-07-25 23:43:33Z ggregory $ + */ +public class CharEncoding { + /** + * CharEncodingISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final String ISO_8859_1 = "ISO-8859-1"; + + /** + *

+ * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final String US_ASCII = "US-ASCII"; + + /** + *

+ * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark + * (either order accepted on input, big-endian used on output) + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final String UTF_16 = "UTF-16"; + + /** + *

+ * Sixteen-bit Unicode Transformation Format, big-endian byte order. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final String UTF_16BE = "UTF-16BE"; + + /** + *

+ * Sixteen-bit Unicode Transformation Format, little-endian byte order. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final String UTF_16LE = "UTF-16LE"; + + /** + *

+ * Eight-bit Unicode Transformation Format. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final String UTF_8 = "UTF-8"; +} diff --git a/test_input/ksa/ksa-dao-context/pom.xml b/test_input/ksa/ksa-dao-context/pom.xml new file mode 100644 index 0000000..5a902b2 --- /dev/null +++ b/test_input/ksa/ksa-dao-context/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + + com.ksa + ksa-root + 3.9.0 + + + ksa-dao-context + jar + + ksa-dao-context + 杭州凯思爱物流管理系统 - DAO 模块运行环境设定 + + + UTF-8 + + + + + + org.mybatis + mybatis + 3.1.1 + + + + org.mybatis + mybatis-spring + 1.1.1 + + + com.ksa + ksa-core + ${project.version} + + + diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/AbstractMybatisDao.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/AbstractMybatisDao.java new file mode 100644 index 0000000..d63b04f --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/AbstractMybatisDao.java @@ -0,0 +1,24 @@ +package com.ksa.dao; + +import org.apache.ibatis.session.SqlSession; + +/** + * 基于 Mybatis 的抽象 Dao 基类。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public abstract class AbstractMybatisDao { + + /** Mybaits SqlSession */ + protected SqlSession session; + + public SqlSession getSqlSession() { + return session; + } + + public void setSqlSession( SqlSession session ) { + this.session = session; + } +} diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/H2LimitDialect.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/H2LimitDialect.java new file mode 100644 index 0000000..938d53b --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/H2LimitDialect.java @@ -0,0 +1,29 @@ +package com.ksa.dao.mybatis.dialect; + +/** + * H2 数据源的分页查询方言。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class H2LimitDialect implements LimitDialect { + + @Override + public boolean supportsLimit() { + return true; + } + + @Override + public String getLimitString( String query, int offset, int limit ) { + if( offset < 0 ) { offset = 0; } + if( limit < 0 ) { limit = 0; } + + StringBuffer sb = new StringBuffer( query.length() + 20 ).append( query ).append( " limit " ).append( limit ); //$NON-NLS-1$ + if( offset > 0 ) { + sb.append( " offset " ).append( offset ); //$NON-NLS-1$ + } + return sb.append( " " ).toString(); //$NON-NLS-1$ + } + +} diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/LimitDialect.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/LimitDialect.java new file mode 100644 index 0000000..414f15f --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/LimitDialect.java @@ -0,0 +1,28 @@ +package com.ksa.dao.mybatis.dialect; + +/** + * SQL分页方言。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public interface LimitDialect { + + /** + * Does this dialect support some form of limiting query results via a SQL clause? + * + * @return True if this dialect supports some form of LIMIT. + */ + boolean supportsLimit(); + + /** + * Given a limit and an offset, apply the limit clause to the query. + * + * @param query The query to which to apply the limit. + * @param offset The offset of the limit + * @param limit The limit of the limit + * @return The modified query statement with the limit applied. + */ + String getLimitString(String query, int offset, int limit); +} diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/MysqlLimitDialect.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/MysqlLimitDialect.java new file mode 100644 index 0000000..b42cc07 --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/MysqlLimitDialect.java @@ -0,0 +1,32 @@ +package com.ksa.dao.mybatis.dialect; + +/** + * Mysql 数据源的分页查询方言。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MysqlLimitDialect implements LimitDialect { + + @Override + public boolean supportsLimit() { + return true; + } + + @Override + public String getLimitString( String query, int offset, int limit ) { + if( offset < 0 ) { offset = 0; } + if( limit < 0 ) { limit = 0; } + + // MySql 分页 select * from table_name limit [offset,] rows + StringBuffer sb = new StringBuffer( query.length() + 20 ).append( query ).append( " limit " ); //$NON-NLS-1$ + if( offset <= 0 ) { + sb.append( limit ); + } else { + sb.append( offset ).append( " , " ).append( limit ); //$NON-NLS-1$ + } + return sb.append( " " ).toString(); //$NON-NLS-1$ + } + +} diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/OracleLimitDialect.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/OracleLimitDialect.java new file mode 100644 index 0000000..13b0888 --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/dialect/OracleLimitDialect.java @@ -0,0 +1,54 @@ +package com.ksa.dao.mybatis.dialect; + +/** + * Oracle 数据源的分页查询方言。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class OracleLimitDialect implements LimitDialect { + + @Override + public boolean supportsLimit() { + return true; + } + + @Override + public String getLimitString( String query, int offset, int limit ) { + if( offset < 0 ) { offset = 0; } + if( limit < 0 ) { limit = 0; } + query = query.trim(); + boolean hasOffset = ( offset > 0 ); + boolean isForUpdate = false; + if( query.toLowerCase().endsWith( " for update" ) ) { + query = query.substring( 0, query.length() - 11 ); + isForUpdate = true; + } + + StringBuffer pagingSelect = new StringBuffer( query.length() + 100 ); + if( hasOffset ) { + pagingSelect.append( "select * from ( select row_.*, rownum rownum_ from ( " ); + } + else { + pagingSelect.append( "select * from ( " ); + } + pagingSelect.append( query ); + // Integer越界检测 + int upperBound = ( Integer.MAX_VALUE - limit < offset ) ? Integer.MAX_VALUE : offset + limit; + if( hasOffset ) { + pagingSelect.append( " ) row_ ) where rownum_ <= " ).append( upperBound ).append( " and rownum_ > " ) + .append( offset ); + } + else { + pagingSelect.append( " ) where rownum <= " ).append( upperBound ); + } + + if( isForUpdate ) { + pagingSelect.append( " for update" ); + } + + return pagingSelect.toString(); + } + +} diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/plugin/PaginationPlugin.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/plugin/PaginationPlugin.java new file mode 100644 index 0000000..0ada267 --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/plugin/PaginationPlugin.java @@ -0,0 +1,184 @@ +package com.ksa.dao.mybatis.plugin; + +import java.io.IOException; +import java.net.URL; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.apache.ibatis.executor.statement.BaseStatementHandler; +import org.apache.ibatis.executor.statement.RoutingStatementHandler; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.RowBounds; + +import com.ksa.dao.mybatis.dialect.LimitDialect; +import com.ksa.dao.mybatis.util.ReflectUtils; + +@Intercepts( { @Signature( type = StatementHandler.class, method = "prepare", args = { Connection.class } ) } ) //$NON-NLS-1$ +/** + * 改进 Mybatis 分页查询的插件。

+ * Mybatis的分页是基于内存分页(查找出所有记录再取出偏移量的记录,如果 jdbc 驱动支持 absolute 定位或者 rs.next() 到指定偏移位置), + * 这样的分页实现性能不佳。

+ * 通过插件将原查询语句进行必要的封转,从而可以利用 jdbc 内置的分页查询支持来提高分页查询的效率。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class PaginationPlugin implements Interceptor { + + private static Log log = LogFactory.getLog( PaginationPlugin.class ); + + /** 长 query 方法中参数的数量。*/ + public static final int LONG_QUERY_PARAMETER_COUNT = 6; + + private Properties properties = new Properties(); + private Map cache = new HashMap(4); + + public PaginationPlugin() { + // 初始化分页方言的实现 + URL url = PaginationPlugin.class.getResource("pagination.properties"); //$NON-NLS-1$ + try { + this.properties.load( url.openStream() ); + } catch( IOException e ) { + log.error( "Initialize pagination dialect implements fail", e ); //$NON-NLS-1$ + } + if( log.isDebugEnabled() ) { + log.debug( "Initialize pagination dialect implements success." ); //$NON-NLS-1$ + } + } + + @Override + public Object intercept( Invocation invocation ) throws Throwable { + StatementHandler statementHandler = (StatementHandler)invocation.getTarget(); + BaseStatementHandler baseHandler = null; + if( statementHandler instanceof RoutingStatementHandler ) { + baseHandler = (BaseStatementHandler)ReflectUtils.getFieldValue( statementHandler, "delegate" ); //$NON-NLS-1$ + } else if( statementHandler instanceof BaseStatementHandler ) { + baseHandler = (BaseStatementHandler)statementHandler; + } else { + // 无法获取 BaseStatementHandler 不进行分页设置 + return invocation.proceed(); + } + + RowBounds rowBounds = (RowBounds)ReflectUtils.getFieldValue( baseHandler, "rowBounds" ); //$NON-NLS-1$ + + if( needPagination( rowBounds ) ) { + BoundSql boundSql = baseHandler.getBoundSql(); + Configuration configuration = (Configuration)ReflectUtils.getFieldValue( baseHandler, "configuration" ); //$NON-NLS-1$ + LimitDialect dialect = getLimitDialect( configuration ); + if( dialect != null && dialect.supportsLimit() ) { + String pageSql = dialect.getLimitString( boundSql.getSql(), rowBounds.getOffset(), rowBounds.getLimit() ); + if( log.isDebugEnabled() ) { + log.debug( "==> Paginating: " + pageSql ); //$NON-NLS-1$ + } + // 将分页sql语句反射回BoundSql + ReflectUtils.setFieldValue( boundSql, "sql", pageSql ); //$NON-NLS-1$ + // 不需要内存分页操作 + ReflectUtils.setFieldValue( rowBounds, "limit", RowBounds.NO_ROW_LIMIT ); //$NON-NLS-1$ + ReflectUtils.setFieldValue( rowBounds, "offset", RowBounds.NO_ROW_OFFSET ); //$NON-NLS-1$ + } + } + + return invocation.proceed(); + } + + @Override + public Object plugin( Object target ) { + return Plugin.wrap( target, this ); + } + + @Override + public void setProperties( Properties properties ) { + if( log.isDebugEnabled() ) { + log.debug( "Set PaginationPlugin properties." ); //$NON-NLS-1$ + } + this.properties.putAll( properties ); + } + + private boolean needPagination( RowBounds rowBounds ) { + return !( rowBounds.getLimit() == RowBounds.NO_ROW_LIMIT && rowBounds.getOffset() == RowBounds.NO_ROW_OFFSET ); + } + + private LimitDialect getLimitDialect( Configuration config ) { + String databaseId = getDatabaseId( config ); + if( cache.containsKey( databaseId ) ) { + if( log.isDebugEnabled() ) { + log.debug( "Get cached LimitDialect object for database type [ "+databaseId+" ]." ); //$NON-NLS-1$ //$NON-NLS-2$ + } + return cache.get( databaseId ); + } + String dialectType = this.properties.getProperty( databaseId ); + if( dialectType == null ) { + log.error( "No LimitDialect is defined for database type [ "+databaseId+" ]." );//$NON-NLS-1$ //$NON-NLS-2$ + } else { + try { + LimitDialect dialect = (LimitDialect)Class.forName( dialectType ).newInstance(); + if( log.isDebugEnabled() ) { + log.debug( "Construct LimitDialect object [ " + dialect.toString() + " ] successful." );//$NON-NLS-1$ //$NON-NLS-2$ + } + cache.put( databaseId, dialect ); + return dialect; + } catch( Exception e ) { + log.error( "Error occured when creating LimitDialect [ " + dialectType + " ] for database type [ " + databaseId + " ]. ", e );//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + return null; + } + + private String getDatabaseId( Configuration config ) { + // config.getDatabaseId() since mybatis-3.1.1 + String databaseId = config.getDatabaseId(); + if( databaseId == null ) { + try { + databaseId = getDatabaseId( config.getEnvironment().getDataSource() ); + } catch( SQLException e ) { + + } + } + if( databaseId != null ) { + databaseId = databaseId.toLowerCase(); + } + return databaseId; + } + + private String getDatabaseProductName( DataSource dataSource ) throws SQLException { + Connection con = null; + try { + con = dataSource.getConnection(); + DatabaseMetaData metaData = con.getMetaData(); + return metaData.getDatabaseProductName(); + } finally { + if( con != null ) { + try { con.close(); } + catch( SQLException e ) { + /* ignored */ + } + } + } + } + + private String getDatabaseId( DataSource dataSource ) throws SQLException { + String productName = getDatabaseProductName( dataSource ); + for( Map.Entry property : this.properties.entrySet() ) { + if( productName.toLowerCase().contains( property.getKey().toString().toLowerCase() ) ) { + return (String)property.getKey(); + } + } + return null; + } +} diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/session/RowBounds.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/session/RowBounds.java new file mode 100644 index 0000000..9c700b2 --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/session/RowBounds.java @@ -0,0 +1,8 @@ +package com.ksa.dao.mybatis.session; + +public class RowBounds extends org.apache.ibatis.session.RowBounds { + + public RowBounds( int page, int rows ) { + super( page * rows - rows, rows ); + } +} diff --git a/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/util/ReflectUtils.java b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/util/ReflectUtils.java new file mode 100644 index 0000000..6c3180a --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/java/com/ksa/dao/mybatis/util/ReflectUtils.java @@ -0,0 +1,99 @@ +package com.ksa.dao.mybatis.util; + +import java.lang.reflect.Field; + +/** + * 反射工具类。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class ReflectUtils { + + /** + * 获取给定 obj 对象名称为 fieldName 的 Field。 + * + * @param obj + * 获取 Field 的目标对象 + * @param fieldName + * Field 对象的名称 + * @return Field 对象 + */ + public static Field getField( Object obj, String fieldName ) { + for( Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass() ) { + try { + return superClass.getDeclaredField( fieldName ); + } catch( NoSuchFieldException e ) { /* 忽略 */} + } + return null; + } + + /** + * 通过反射方式获取给定 obj 对象 field 字段的值。 + * @param obj 获取所需字段值的目标对象 + * @param field 目标字段 + * @return 指定字段的值 + * @throws IllegalArgumentException + * @throws IllegalAccessException + */ + public static Object getFieldValue( Object obj, Field field ) throws IllegalArgumentException, IllegalAccessException { + Object value = null; + if( field != null ) { + if( field.isAccessible() ) { + value = field.get( obj ); + } else { + field.setAccessible( true ); + value = field.get( obj ); + field.setAccessible( false ); + } + } + return value; + } + + /** + * 通过反射方式获取给定 obj 对象名称为 fieldName 的字段值。 + * + * @param obj 获取所需字段值的目标对象 + * @param fieldName 目标字段名称 + * @return 指定字段的值 + * @throws IllegalArgumentException + * @throws IllegalAccessException + */ + public static Object getFieldValue( Object obj, String fieldName ) throws IllegalArgumentException, IllegalAccessException { + return getFieldValue( obj, getField( obj, fieldName ) ); + } + + /** + * 通过反射方式将给定 obj 对象的 field 字段值设置为 value。 + * @param obj 设置所需字段值的目标对象 + * @param field 目标字段 + * @param value 目标字段值 + * @throws IllegalArgumentException + * @throws IllegalAccessException + */ + public static void setFieldValue( Object obj, Field field, Object value ) throws IllegalArgumentException, IllegalAccessException { + if( field == null ) { + throw new IllegalArgumentException( "Field argument could not be null." ); //$NON-NLS-1$ + } + if( field.isAccessible() ) { + field.set( obj, value ); + } else { + field.setAccessible( true ); + field.set( obj, value ); + field.setAccessible( false ); + } + } + + /** + * 通过反射方式将给定 obj 对象名称为 field 的字段值设置为 value。 + * @param obj 设置所需字段值的目标对象 + * @param fieldName 目标字段名称 + * @param value 目标字段值 + * @throws IllegalArgumentException + * @throws IllegalAccessException + */ + public static void setFieldValue( Object obj, String fieldName, Object value ) throws IllegalArgumentException, IllegalAccessException { + setFieldValue( obj, getField( obj, fieldName ), value); + } +} diff --git a/test_input/ksa/ksa-dao-context/src/main/resources/com/ksa/dao/mybatis/plugin/pagination.properties b/test_input/ksa/ksa-dao-context/src/main/resources/com/ksa/dao/mybatis/plugin/pagination.properties new file mode 100644 index 0000000..c02f8cf --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/resources/com/ksa/dao/mybatis/plugin/pagination.properties @@ -0,0 +1,5 @@ +## Implementations of LimitDialect with specific database id. +## Can be overridden or appended by plugins settings in mybatis-config.xml +oracle=com.ksa.dao.mybatis.dialect.OracleLimitDialect +mysql=com.ksa.dao.mybatis.dialect.MysqlLimitDialect +h2=com.ksa.dao.mybatis.dialect.H2LimitDialect \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-context/src/main/resources/mybatis/mybatis-config.xml b/test_input/ksa/ksa-dao-context/src/main/resources/mybatis/mybatis-config.xml new file mode 100644 index 0000000..24c1467 --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/resources/mybatis/mybatis-config.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-context/src/main/resources/spring/dao/dao-config.xml b/test_input/ksa/ksa-dao-context/src/main/resources/spring/dao/dao-config.xml new file mode 100644 index 0000000..48fb1a9 --- /dev/null +++ b/test_input/ksa/ksa-dao-context/src/main/resources/spring/dao/dao-config.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/pom.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/pom.xml new file mode 100644 index 0000000..765a77e --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.ksa + ksa-dao-root + 3.9.0 + + + ksa-bd-dao + jar + + ksa-bd-dao + 杭州凯思爱物流管理系统 - 基础数据管理 DAO 模块 + + + UTF-8 + + diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDao.java b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDao.java new file mode 100644 index 0000000..c35da07 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDao.java @@ -0,0 +1,48 @@ +package com.ksa.dao.bd.mybatis; + +import java.util.List; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.bd.BasicDataDao; +import com.ksa.model.bd.BasicData; + +/** + * 基于 Mybaits 的 BasicDataDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisBasicDataDao extends AbstractMybatisDao implements BasicDataDao { + + @Override + public int insertBasicData( BasicData data ) throws RuntimeException { + return this.session.insert( "insert-bd-data", data ); + } + + @Override + public int updateBasicData( BasicData data ) throws RuntimeException { + return this.session.update( "update-bd-data", data ); + } + + @Override + public int deleteBasicData( BasicData data ) throws RuntimeException { + return this.session.delete( "delete-bd-data", data ); + } + + @Override + public BasicData selectBasicDataById( String id ) throws RuntimeException { + return this.session.selectOne( "select-bd-data-byid", id ); + } + + @Override + public List selectBasicDataByType( String typeId ) throws RuntimeException { + return this.session.selectList( "select-bd-data-bytype", typeId ); + } + + @Override + public List selectAllBasicData() throws RuntimeException { + return this.session.selectList( "select-bd-data-all" ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDao.java b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDao.java new file mode 100644 index 0000000..d21b612 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDao.java @@ -0,0 +1,132 @@ +package com.ksa.dao.bd.mybatis; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.StringUtils; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.bd.CurrencyRateDao; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.CurrencyRate; + +/** + * 基于 Mybaits 的 CurrencyRateDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisCurrencyRateDao extends AbstractMybatisDao implements CurrencyRateDao { + + @Override + public int insertRate( CurrencyRate rate ) throws RuntimeException { + if( rate.getMonth() == null ) { + return this.session.insert( "insert-bd-rate-bypartner", rate ); + } else { + return this.session.insert( "insert-bd-rate-bydate", rate ); + } + } + + @Override + public int updateRate( CurrencyRate rate ) throws RuntimeException { + // 先尝试按日期更新 + int count = this.session.update( "update-bd-rate-bydate", rate ); + if( count == 0 ) { + return this.session.update( "update-bd-rate-bypartner", rate ); + } + return count; + } + + @Override + public int deleteRate( CurrencyRate rate ) throws RuntimeException { + CurrencyRate c = this.session.selectOne( "select-bd-rate-bydate-byid", rate.getId() ); + if( c == null ) { + return this.session.delete( "delete-bd-rate-bypartner", rate ); + } + return this.session.delete( "delete-bd-rate-bydate", rate ); + } + + @Override + public Currency selectCurrencyById( String currencyId ) throws RuntimeException { + return this.session.selectOne( "select-bd-currency", currencyId ); + } + + @Override + public List selectAllCurrency() throws RuntimeException { + return this.session.selectList( "select-bd-currency" ); + } + + @Override + public List selectLatestRates() throws RuntimeException { + return this.selectLatestRates( new Date() ); + } + + @Override + public List selectLatestRates( Date date ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "date", date ); + return this.session.selectList( "select-bd-rate-latest", paras ); + } + + @Override + public CurrencyRate selectLatestRate( String currencyId, Date date ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "date", date ); + paras.put( "currencyId", currencyId ); + return this.session.selectOne( "select-bd-rate-latest", paras ); + } + + @Override + public CurrencyRate selectLatestRate( String currencyId ) throws RuntimeException { + return this.selectLatestRate( currencyId, new Date() /* now */ ); + } + + @Override + public List selectRateByDate( Date start, Date end ) throws RuntimeException { + return selectRateByDate( start, end, null ); + } + + @Override + public List selectRateByDate( Date start, Date end, String currencyId ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "startDate", start ); + paras.put( "endDate", end ); + if( StringUtils.hasText( currencyId ) ) { + paras.put( "currencyId", currencyId ); + } + return this.session.selectList( "select-bd-rate-bydate", paras ); + } + + @Override + public List selectRateByPartner( String partnerId ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "partnerId", partnerId ); + return this.session.selectList( "select-bd-rate-bypartner", paras ); + } + + @Override + public CurrencyRate selectRateByPartner( String partnerId, String currencyId ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "partnerId", partnerId ); + paras.put( "currencyId", currencyId ); + return this.session.selectOne( "select-bd-rate-bypartner", paras ); + } + + @Override + public CurrencyRate selectRateById( String id ) throws RuntimeException { + CurrencyRate rate = this.session.selectOne( "select-bd-rate-bydate-byid", id ); + if( rate == null ) { + return this.session.selectOne( "select-bd-rate-bypartner-byid", id ); + } + return rate; + } + + @Override + public List selectAllRates() throws RuntimeException { + return this.session.selectList( "select-bd-rate-all" ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisPartnerDao.java b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisPartnerDao.java new file mode 100644 index 0000000..49f7caf --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/java/com/ksa/dao/bd/mybatis/MybatisPartnerDao.java @@ -0,0 +1,77 @@ +package com.ksa.dao.bd.mybatis; + +import java.util.HashMap; +import java.util.Map; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.bd.PartnerDao; +import com.ksa.model.bd.Partner; +import com.ksa.model.bd.PartnerType; + +/** + * 基于 Mybaits 的合作伙伴数据访问接口实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisPartnerDao extends AbstractMybatisDao implements PartnerDao { + + @Override + public int insertPartner( Partner partner ) throws RuntimeException { + return this.session.insert( "insert-bd-partner", partner ); + } + + @Override + public int updatePartner( Partner partner ) throws RuntimeException { + return this.session.update( "update-bd-partner", partner ); + } + + @Override + public int deletePartner( Partner partner ) throws RuntimeException { + return this.session.delete( "delete-bd-partner", partner ); + } + + @Override + public Partner selectPartnerById( String id ) throws RuntimeException { + return this.session.selectOne( "select-bd-partner-byid", id ); + } + + @Override + public Partner selectPartnerByCode( String code ) throws RuntimeException { + return this.session.selectOne( "select-bd-partner-bycode", code ); + } + + @Override + public int insertPartnerType( Partner partner, PartnerType type ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "partneId", partner.getId() ); + paras.put( "typeId", type.getId() ); + return this.session.insert( "insert-bd-partnertype", paras ); + } + + @Override + public int deletePartnerType( Partner partner, PartnerType type ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "partneId", partner.getId() ); + paras.put( "typeId", type.getId() ); + return this.session.delete( "delete-bd-partnertype", paras ); + } + + @Override + public int insertPartnerExtra( Partner partner, String extra ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "partneId", partner.getId() ); + paras.put( "extra", extra ); + return this.session.insert( "insert-bd-partnertextra", paras ); + } + + @Override + public int deletePartnerExtra( Partner partner, String extra ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "partneId", partner.getId() ); + paras.put( "extra", extra ); + return this.session.delete( "delete-bd-partnertextra", paras ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency-rate.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency-rate.xml new file mode 100644 index 0000000..2d24b06 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency-rate.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_BD_CURRENCY_RATE_BYDATE + ( ID, CURRENCY_ID, MONTH, RATE ) + VALUES ( #{id,jdbcType=VARCHAR}, #{currency.id,jdbcType=VARCHAR}, #{month,jdbcType=DATE}, #{rate} ) + + + + INSERT INTO KSA_BD_CURRENCY_RATE_BYPARTNER + ( ID, CURRENCY_ID, PARTNER_ID, RATE ) + VALUES ( #{id,jdbcType=VARCHAR}, #{currency.id,jdbcType=VARCHAR}, #{partner.id,jdbcType=VARCHAR}, #{rate} ) + + + + + UPDATE KSA_BD_CURRENCY_RATE_BYDATE SET + RATE = #{rate} + WHERE ID = #{id} + + + + UPDATE KSA_BD_CURRENCY_RATE_BYPARTNER SET + RATE = #{rate} + WHERE ID = #{id} + + + + + DELETE FROM KSA_BD_CURRENCY_RATE_BYDATE WHERE ID = #{id} + + + + DELETE FROM KSA_BD_CURRENCY_RATE_BYPARTNER WHERE ID = #{id} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency.xml new file mode 100644 index 0000000..a3ee324 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-currency.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-data.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-data.xml new file mode 100644 index 0000000..acb3d4b --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-data.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_BD_DATA + ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) + VALUES ( #{id}, #{code}, #{name}, #{alias,jdbcType=VARCHAR}, #{note,jdbcType=VARCHAR}, #{extra,jdbcType=VARCHAR}, #{type.id}, #{rank} ) + + + + + UPDATE KSA_BD_DATA SET + CODE = #{code}, + NAME = #{name}, + ALIAS = #{alias,jdbcType=VARCHAR}, + NOTE = #{note,jdbcType=VARCHAR}, + EXTRA = #{extra,jdbcType=VARCHAR}, + RANK = #{rank} + WHERE ID = #{id} + + + + + DELETE FROM KSA_BD_DATA WHERE ID = #{id} + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-extra.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-extra.xml new file mode 100644 index 0000000..bbda008 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-extra.xml @@ -0,0 +1,38 @@ + + + + + + + + + INSERT INTO KSA_BD_PARTNER_EXTRA ( PARTNER_ID, EXTRA ) + VALUES ( #{partneId,jdbcType=VARCHAR}, #{extra,jdbcType=VARCHAR} ) + + + + DELETE FROM KSA_BD_PARTNER_EXTRA + WHERE PARTNER_ID = #{partneId,jdbcType=VARCHAR} + AND EXTRA = #{extra,jdbcType=VARCHAR} + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-type.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-type.xml new file mode 100644 index 0000000..d68bdfb --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner-type.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_BD_PARTNER_TYPE ( PARTNER_ID, TYPE_ID ) + VALUES ( #{partneId,jdbcType=VARCHAR}, #{typeId,jdbcType=VARCHAR} ) + + + + DELETE FROM KSA_BD_PARTNER_TYPE + WHERE PARTNER_ID = #{partneId,jdbcType=VARCHAR} + AND TYPE_ID = #{typeId,jdbcType=VARCHAR} + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner.xml new file mode 100644 index 0000000..6ec6671 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/mybatis/mapper/bd-partner.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_BD_PARTNER + ( ID, CODE, NAME, ALIAS, ADDRESS, PP, NOTE, IMPORTANT, RANK, SALER_ID ) + VALUES ( #{id,jdbcType=VARCHAR}, #{code,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{alias,jdbcType=VARCHAR}, #{address,jdbcType=VARCHAR}, + #{pp, jdbcType=NUMERIC}, #{note,jdbcType=VARCHAR}, #{important, jdbcType=NUMERIC}, #{rank, jdbcType=NUMERIC}, #{saler.id,jdbcType=VARCHAR} ) + + + + + UPDATE KSA_BD_PARTNER SET + CODE = #{code,jdbcType=VARCHAR}, + NAME = #{name,jdbcType=VARCHAR}, + ALIAS = #{alias,jdbcType=VARCHAR}, + ADDRESS = #{address,jdbcType=VARCHAR}, + PP = #{pp, jdbcType=NUMERIC}, + NOTE = #{note,jdbcType=VARCHAR}, + IMPORTANT = #{important, jdbcType=NUMERIC}, + RANK = #{rank, jdbcType=NUMERIC}, + SALER_ID = #{saler.id,jdbcType=VARCHAR} + WHERE ID = #{id} + + + + + UPDATE KSA_BD_PARTNER SET + IMPORTANT = -1 + WHERE ID = #{id} + + + + SELECT a.ID, a.CODE, a.NAME, a.ALIAS, a.ADDRESS, a.PP, a.NOTE, a.IMPORTANT, a.RANK, + s.ID AS SALER_ID, s.NAME AS SALER_NAME + FROM KSA_BD_PARTNER a + LEFT JOIN KSA_SECURITY_USER s ON a.SALER_ID = s.ID + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/spring/dao/bd-dao-context.xml b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/spring/dao/bd-dao-context.xml new file mode 100644 index 0000000..d177efe --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/main/resources/spring/dao/bd-dao-context.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDaoTest.java new file mode 100644 index 0000000..f2daad9 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisBasicDataDaoTest.java @@ -0,0 +1,81 @@ +package com.ksa.dao.bd.mybatis; + +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.bd.BasicDataDao; +import com.ksa.model.bd.BasicData; +import com.ksa.model.bd.BasicDataType; + +public class MybatisBasicDataDaoTest extends MybatisDaoTest { + + @Test + public void testSelectBasicDataById() throws RuntimeException { + BasicDataDao dao = CONTEXT.getBean( "basicDataDao", BasicDataDao.class ); + BasicData data = dao.selectBasicDataById( "00-currency-RMB" ); + Assert.assertEquals( "RMB", data.getCode() ); + Assert.assertEquals( "人民币", data.getName() ); + Assert.assertEquals( "", data.getAlias() ); + Assert.assertEquals( "", data.getNote() ); + Assert.assertEquals( "1.000", data.getExtra() ); + Assert.assertEquals( BasicDataType.CURRENCY.getId(), data.getType().getId() ); + Assert.assertEquals( BasicDataType.CURRENCY.getName(), data.getType().getName() ); + } + + @Test + public void testSelectBasicDataByType() throws RuntimeException { + BasicDataDao dao = CONTEXT.getBean( "basicDataDao", BasicDataDao.class ); + dao.selectBasicDataByType( BasicDataType.CURRENCY.getId() ); + } + + @Test + public void testCrudBasicData() throws RuntimeException { + BasicDataDao dao = CONTEXT.getBean( "basicDataDao", BasicDataDao.class ); + + BasicData data = new BasicData(); + String id = UUID.randomUUID().toString(); + data.setId( id ); + data.setCode( "code" ); + data.setName( "name" ); + data.setAlias( "alias" ); + data.setExtra( "extra" ); + data.setNote( "note" ); + data.setType( BasicDataType.UNITS ); + + // 测试插入是否成功 + dao.insertBasicData( data ); + BasicData temp = dao.selectBasicDataById( id ); + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "name", temp.getName() ); + Assert.assertEquals( "alias", temp.getAlias() ); + Assert.assertEquals( "note", temp.getNote() ); + Assert.assertEquals( "extra", temp.getExtra() ); + Assert.assertEquals( BasicDataType.UNITS.getId(), temp.getType().getId() ); + Assert.assertEquals( BasicDataType.UNITS.getName(), temp.getType().getName() ); + + data.setCode( "code1" ); + data.setName( "name1" ); + data.setAlias( "alias1" ); + data.setExtra( "extra1" ); + data.setNote( "note1" ); + + // 测试更新是否成功 + dao.updateBasicData( data ); + BasicData temp2 = dao.selectBasicDataById( id ); + Assert.assertNotNull( temp2 ); + Assert.assertEquals( "code1", temp2.getCode() ); + Assert.assertEquals( "name1", temp2.getName() ); + Assert.assertEquals( "alias1", temp2.getAlias() ); + Assert.assertEquals( "note1", temp2.getNote() ); + Assert.assertEquals( "extra1", temp2.getExtra() ); + + // 测试删除 + dao.deleteBasicData( data ); + BasicData temp3 = dao.selectBasicDataById( id ); + Assert.assertNull( temp3 ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDaoTest.java new file mode 100644 index 0000000..48901a2 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisCurrencyRateDaoTest.java @@ -0,0 +1,174 @@ +package com.ksa.dao.bd.mybatis; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.bd.CurrencyRateDao; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.bd.Partner; + +public class MybatisCurrencyRateDaoTest extends MybatisDaoTest { + + private static final Currency TEST_CURRENCY = new Currency(); + private static final Currency TEST_CURRENCY2 = new Currency(); + private static final Partner TEST_PARTNER = new Partner(); + static { + TEST_CURRENCY.setId( "00-currency-USD" ); + TEST_CURRENCY.setCode( "USD" ); + TEST_CURRENCY.setName( "美元" ); + TEST_CURRENCY.setExtra( "6.800" ); + + TEST_CURRENCY2.setId( "00-currency-HKD" ); + TEST_CURRENCY2.setCode( "HKD" ); + TEST_CURRENCY2.setName( "港币" ); + TEST_CURRENCY2.setExtra( "1.3" ); + + TEST_PARTNER.setId( "test-partner-1" ); + TEST_PARTNER.setCode( "test-partner-1" ); + TEST_PARTNER.setName( "测试合作伙伴1" ); + } + + @SuppressWarnings( "deprecation" ) + @Test + public void testSelectLatestRate() throws RuntimeException { + CurrencyRateDao dao = CONTEXT.getBean( "currencyRateDao", CurrencyRateDao.class ); + + // 美元 10月1日 + CurrencyRate rate = new CurrencyRate(); + rate.setId( UUID.randomUUID().toString() ); + rate.setCurrency( TEST_CURRENCY ); + rate.setMonth( new Date( 2012, 9, 1 ) ); + rate.setRate( 10.1f ); + dao.insertRate( rate ); + // 美元 11月1日 + rate = new CurrencyRate(); + rate.setId( UUID.randomUUID().toString() ); + rate.setCurrency( TEST_CURRENCY ); + rate.setMonth( new Date( 2012, 10, 1 ) ); + rate.setRate( 11.1f ); + dao.insertRate( rate ); + // 美元 12月1日 + rate = new CurrencyRate(); + rate.setId( UUID.randomUUID().toString() ); + rate.setCurrency( TEST_CURRENCY ); + rate.setMonth( new Date( 2012, 11, 1 ) ); + rate.setRate( 12.1f ); + dao.insertRate( rate ); + + // 港币 9月1日 + rate = new CurrencyRate(); + rate.setId( UUID.randomUUID().toString() ); + rate.setCurrency( TEST_CURRENCY2 ); + rate.setMonth( new Date( 2012, 8, 1 ) ); + rate.setRate( 0.81f ); + dao.insertRate( rate ); + // 港币 10月1日 + rate = new CurrencyRate(); + rate.setId( UUID.randomUUID().toString() ); + rate.setCurrency( TEST_CURRENCY2 ); + rate.setMonth( new Date( 2012, 9, 1 ) ); + rate.setRate( 0.91f ); + dao.insertRate( rate ); + + // 港币 11月1日 + rate = new CurrencyRate(); + rate.setId( UUID.randomUUID().toString() ); + rate.setCurrency( TEST_CURRENCY2 ); + rate.setMonth( new Date( 2012, 10, 1 ) ); + rate.setRate( 1.01f ); + dao.insertRate( rate ); + + // 开始测试 + // 1. 11月(包含)之前最近的所有汇率 + List list = dao.selectLatestRates( new Date( 2012, 10, 1 ) ); + Assert.assertEquals( 2, list.size() ); + Assert.assertEquals( 11.1f, list.get( 0 ).getRate(), 0.001 ); // 美元11月的汇率 + Assert.assertEquals( 1.01f, list.get( 1 ).getRate(), 0.001 ); // 港币11月的汇率 + + // 2. 12月(包含)之前最近的所有汇率 + list = dao.selectLatestRates( new Date( 2012, 11, 1 ) ); + Assert.assertEquals( 2, list.size() ); + Assert.assertEquals( 12.1f, list.get( 0 ).getRate(), 0.001 ); // 美元12月的汇率 + Assert.assertEquals( 1.01f, list.get( 1 ).getRate(), 0.001 ); // 港币11月的汇率 + + // 3. 9月(包含)之前最近的所有汇率 + list = dao.selectLatestRates( new Date( 2012, 8, 1 ) ); + Assert.assertEquals( 1, list.size() ); + Assert.assertEquals( 0.81f, list.get( 0 ).getRate(), 0.001 ); // 港币9月的汇率 + } + + @Test + public void testCrudCurrencyRateByDate() throws RuntimeException { + CurrencyRateDao dao = CONTEXT.getBean( "currencyRateDao", CurrencyRateDao.class ); + + CurrencyRate rate = new CurrencyRate(); + String id = UUID.randomUUID().toString(); + @SuppressWarnings( "deprecation" ) + Date now = new Date( 1985, 10, 28 ); + rate.setId( id ); + rate.setCurrency( TEST_CURRENCY ); + rate.setRate( 123.456f ); + rate.setMonth( now ); + + + // 测试插入是否成功 + dao.insertRate( rate ); + CurrencyRate temp = dao.selectRateById( id ); + Assert.assertEquals( now, temp.getMonth() ); + Assert.assertEquals( 123.456f, temp.getRate(), 0.001 ); + Assert.assertEquals( TEST_CURRENCY.getId(), temp.getCurrency().getId() ); + + rate.setRate( 654.321f ); + + // 测试更新是否成功 + Assert.assertEquals( 1, dao.updateRate( rate ) ); + CurrencyRate temp2 = dao.selectRateById( id ); + Assert.assertNotNull( temp2 ); + Assert.assertEquals( 654.321f, temp2.getRate(), 0.001 ); + + // 测试删除 + Assert.assertEquals( 1, dao.deleteRate( rate ) ); + CurrencyRate temp3 = dao.selectRateById( id ); + Assert.assertNull( temp3 ); + } + + @Test + public void testCrudCurrencyRateByPartner() throws RuntimeException { + CurrencyRateDao dao = CONTEXT.getBean( "currencyRateDao", CurrencyRateDao.class ); + + CurrencyRate rate = new CurrencyRate(); + String id = UUID.randomUUID().toString(); + rate.setId( id ); + rate.setCurrency( TEST_CURRENCY ); + rate.setRate( 123.456f ); + rate.setPartner( TEST_PARTNER ); + + + // 测试插入是否成功 + dao.insertRate( rate ); + CurrencyRate temp = dao.selectRateById( id ); + Assert.assertEquals( TEST_PARTNER.getId(), temp.getPartner().getId() ); + Assert.assertEquals( 123.456f, temp.getRate(), 0.001 ); + Assert.assertEquals( TEST_CURRENCY.getId(), temp.getCurrency().getId() ); + + rate.setRate( 654.321f ); + + // 测试更新是否成功 + Assert.assertEquals( 1, dao.updateRate( rate ) ); + CurrencyRate temp2 = dao.selectRateById( id ); + Assert.assertNotNull( temp2 ); + Assert.assertEquals( 654.321f, temp2.getRate(), 0.001 ); + + // 测试删除 + Assert.assertEquals( 1, dao.deleteRate( rate ) ); + CurrencyRate temp3 = dao.selectRateById( id ); + Assert.assertNull( temp3 ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisPartnerDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisPartnerDaoTest.java new file mode 100644 index 0000000..c75182f --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/java/com/ksa/dao/bd/mybatis/MybatisPartnerDaoTest.java @@ -0,0 +1,138 @@ +package com.ksa.dao.bd.mybatis; + +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.bd.PartnerDao; +import com.ksa.model.bd.Partner; +import com.ksa.model.bd.PartnerType; + +public class MybatisPartnerDaoTest extends MybatisDaoTest { + + @Test + public void testSelectPartnerById() throws RuntimeException { + PartnerDao dao = CONTEXT.getBean( "partnerDao", PartnerDao.class ); + Partner data = dao.selectPartnerById( "test-partner-1" ); + Assert.assertEquals( "test-partner-1", data.getCode() ); + Assert.assertEquals( "测试合作伙伴1", data.getName() ); + Assert.assertEquals( "Alias1-1", data.getAlias() ); + Assert.assertEquals( "", data.getAddress() ); + Assert.assertEquals( "", data.getNote() ); + Assert.assertEquals( 30, data.getPp() ); + Assert.assertEquals( 1, data.getRank() ); + Assert.assertTrue( data.getImportant() == 1 ); + Assert.assertEquals( "test-user-1", data.getSaler().getId() ); + String[] extras = data.getExtras(); + Assert.assertEquals( 2, extras.length ); + Assert.assertEquals( "extra1-1", extras[0] ); + Assert.assertEquals( "extra1-2", extras[1] ); + + PartnerType[] types = data.getTypes(); + Assert.assertEquals( 1, types.length ); + Assert.assertEquals( "20-department-bgh", types[0].getId() ); + Assert.assertEquals( "BGH", types[0].getCode() ); + Assert.assertEquals( "报关行", types[0].getName() ); + } + + @Test + public void testCrudPartner() throws RuntimeException { + PartnerDao dao = CONTEXT.getBean( "partnerDao", PartnerDao.class ); + + Partner partner = new Partner(); + String id = UUID.randomUUID().toString(); + partner.setId( id ); + partner.setCode( "code" ); + partner.setName( "name" ); + partner.setAlias( "alias" ); + partner.setAddress( "address" ); + partner.setNote( "note" ); + partner.setPp( 30 ); + partner.setRank( 100 ); + partner.setImportant( 0 ); + partner.getSaler().setId( "test-user-1" ); + + PartnerType type1 = new PartnerType(); + type1.setId( "20-department-dls" ); + PartnerType type2 = new PartnerType(); + type2.setId( "20-department-gys" ); + + // 测试插入是否成功 + dao.insertPartner( partner ); + dao.insertPartnerExtra( partner, "extra1" ); + dao.insertPartnerExtra( partner, "extra2" ); + dao.insertPartnerType( partner, type1 ); + dao.insertPartnerType( partner, type2 ); + + Partner temp = dao.selectPartnerById( id ); + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "name", temp.getName() ); + Assert.assertEquals( "alias", temp.getAlias() ); + Assert.assertEquals( "address", temp.getAddress() ); + Assert.assertEquals( "note", temp.getNote() ); + Assert.assertEquals( 30, temp.getPp() ); + Assert.assertEquals( 100, temp.getRank() ); + Assert.assertTrue( temp.getImportant() == 0 ); + Assert.assertEquals( "test-user-1", temp.getSaler().getId() ); + + String[] extras = temp.getExtras(); + Assert.assertEquals( 2, extras.length ); + Assert.assertEquals( "extra1", extras[0] ); + Assert.assertEquals( "extra2", extras[1] ); + + PartnerType[] types = temp.getTypes(); + Assert.assertEquals( 2, types.length ); + Assert.assertEquals( "20-department-dls", types[0].getId() ); + Assert.assertEquals( "DLS", types[0].getCode() ); + Assert.assertEquals( "代理商", types[0].getName() ); + Assert.assertEquals( "20-department-gys", types[1].getId() ); + Assert.assertEquals( "GYS", types[1].getCode() ); + Assert.assertEquals( "供应商", types[1].getName() ); + + + // 开始更新 + partner.setCode( "code1" ); + partner.setName( "name1" ); + partner.setAlias( "alias1" ); + partner.setAddress( "address1" ); + partner.setNote( "note1" ); + partner.setImportant( 1 ); + partner.setPp( 300 ); + partner.setRank( 1000 ); + partner.getSaler().setId( "test-user-2" ); + + // 测试更新是否成功 + dao.updatePartner( partner ); + dao.deletePartnerExtra( partner, "extra2" ); + dao.deletePartnerType( partner, type2 ); + + temp = dao.selectPartnerById( id ); + Assert.assertEquals( "code1", temp.getCode() ); + Assert.assertEquals( "name1", temp.getName() ); + Assert.assertEquals( "alias1", temp.getAlias() ); + Assert.assertEquals( "address1", temp.getAddress() ); + Assert.assertEquals( "note1", temp.getNote() ); + Assert.assertEquals( 300, temp.getPp() ); + Assert.assertEquals( 1000, temp.getRank() ); + Assert.assertTrue( temp.getImportant() == 1 ); + Assert.assertEquals( "test-user-2", temp.getSaler().getId() ); + + extras = temp.getExtras(); + Assert.assertEquals( 1, extras.length ); + Assert.assertEquals( "extra1", extras[0] ); + + types = temp.getTypes(); + Assert.assertEquals( 1, types.length ); + Assert.assertEquals( "20-department-dls", types[0].getId() ); + Assert.assertEquals( "DLS", types[0].getCode() ); + Assert.assertEquals( "代理商", types[0].getName() ); + + // 测试删除 + dao.deletePartner( partner ); + Partner temp3 = dao.selectPartnerById( id ); + Assert.assertTrue(temp3.getImportant() == -1 ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/resources/init.sql b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/resources/init.sql new file mode 100644 index 0000000..5e7e79b --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-bd-dao/src/test/resources/init.sql @@ -0,0 +1,509 @@ +------------------------ 测试相关表及数据 ---------------------------- +-- 创建表 - 用户表 +create table KSA_SECURITY_USER ( + ID varchar(36) primary key, + NAME varchar(256), + PASSWORD varchar(256), + EMAIL varchar(256), + TELEPHONE varchar(256) +); + + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-1', '麻文强', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456' ); + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-2', '科比 - 布莱恩特', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456' ); + +------------------------ 测试相关表及数据 ---------------------------- + + +-- 创建表 - 基础数据类型 +create table KSA_BD_TYPE ( + ID varchar(36) not null comment '数据类型标识', + NAME varchar(200) not null comment '数据类型名称', + primary key ( ID ) +); + + insert into KSA_BD_TYPE ( ID, NAME ) values ( '00-currency', '结算货币' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '01-units', '数量单位' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '08-vehicle', '车辆类型' ); + -- insert into KSA_BD_TYPE ( ID, NAME ) values ( '09-custom', '报关类型' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '10-charge', '费用类型' ); + -- insert into KSA_BD_TYPE ( ID, NAME ) values ( '11-trade-clause', '贸易条款' ); + -- insert into KSA_BD_TYPE ( ID, NAME ) values ( '12-freight-clause', '运费条款' ); + -- insert into KSA_BD_TYPE ( ID, NAME ) values ( '13-surcharge-clause', '附加费条款' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '20-department', '单位类型' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '30-state', '国家地区' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '31-port-sea', '海运港口' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '32-port-air', '空运港口' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '33-route-sea', '海运航线' ); + insert into KSA_BD_TYPE ( ID, NAME ) values ( '34-route-air', '空运航线' ); + + +-- 创建表 - 基础数据 +create table KSA_BD_DATA ( + ID varchar(36) not null comment '标识', + CODE varchar(200) not null comment '编码', + NAME varchar(200) not null comment '名称', + ALIAS varchar(200) not null comment '别名', + NOTE varchar(2000) not null comment '备注', + EXTRA varchar(200) not null comment '附加属性', + TYPE_ID varchar(36) not null comment '类型标识', + RANK int not null comment '排序', + primary key ( ID ) +); + + -- 币种 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-RMB', 'RMB', '人民币', '', '', '1.000', '00-currency', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-USD', 'USD', '美元', '', '', '6.800', '00-currency', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-HKD', 'HKD', '港币', '', '', '0.882', '00-currency', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-JPY', 'JPY', '日元', '', '', '0.075', '00-currency', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-EUR', 'EUR', '欧元', '', '', '10.000', '00-currency', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-TWD', 'TWD', '台币', '', '', '0.200', '00-currency', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-KRW', 'KRW', '韩元', '', '', '0.005', '00-currency', 7 ); + -- 数量单位 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '01-units-0', 'CASE', '箱', '', '', '', '01-units', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '01-units-1', 'CTNS', '纸箱', '', '', '', '01-units', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '01-units-2', 'PLTS', '托盘', '', '', '', '01-units', 3 ); + -- 报关 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '09-custom-0', 'QG', '清关', '', '', '', '09-custom', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '09-custom-1', 'ZG', '转关', '', '', '', '09-custom', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '09-custom-2', 'WGQ', '外高桥', '', '', '', '09-custom', 3 ); + -- 费用类型 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-001', 'ABF', '安保费', '', '', '', '10-charge', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-002', 'AFC', 'Air/Ocean Freight Charge', '', '', '', '10-charge', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-003', 'AWC', 'Air Waybill Charge(AWC)', '', '', '', '10-charge', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-004', 'BAF', 'BAF', '', '', '', '10-charge', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-005', 'BDF', '并单费', '', '', '', '10-charge', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-006', 'BGF1', '包干费', '', '', '', '10-charge', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-007', 'BGF2', '报关费', '', '', '', '10-charge', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-008', 'BHF', '驳货费', '', '', '', '10-charge', 8 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-009', 'BSCC', '保税仓储费', '', '', '', '10-charge', 9 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-010', 'BXF', '保险费', '', '', '', '10-charge', 10 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-011', 'BZF', '包装费', '', '', '', '10-charge', 11 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-012', 'BZF1', '办证费', '', '', '', '10-charge', 12 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-013', 'CCF', '仓储费', '', '', '', '10-charge', 13 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-014', 'CCF1', '铲车费', '', '', '', '10-charge', 14 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-015', 'CCFGW','车船费(国外)', '', '', '', '10-charge', 15 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-016', 'CCFWN','车船费(国内)', '', '', '', '10-charge', 16 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-017', 'CDF', '仓单费', '', '', '', '10-charge', 17 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-018', 'CFS', 'CFS Charge', '', '', '', '10-charge', 18 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-019', 'CGF', '冲关费', '', '', '', '10-charge', 19 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-020', 'CJF', '磁检费', '', '', '', '10-charge', 20 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-021', 'CKBGF','出口报关费', '', '', '', '10-charge', 21 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-022', 'CKCZ', '仓库操作费', '', '', '', '10-charge', 22 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-023', 'CKXGYWDLF','出口相关业务代理费', '', '', '', '10-charge', 23 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-024', 'CKXKZSQF','出口许可证申请费', '', '', '', '10-charge', 24 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-025', 'CKZF', '出口杂费', '', '', '', '10-charge', 25 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-026', 'CLF', '材料费', '', '', '', '10-charge', 26 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-027', 'CXF', '查询费', '', '', '', '10-charge', 27 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-028', 'CXF1', '拆箱费', '', '', '', '10-charge', 28 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-029', 'CYF', '查验费', '', '', '', '10-charge', 29 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-030', 'CYF1', '检验费(植物)', '', '', '', '10-charge', 30 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-031', 'CYF2', '检验费(动物)', '', '', '', '10-charge', 31 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-032', 'DBF', '短驳费', '', '', '', '10-charge', 32 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-033', 'DBF1', '代办费', '', '', '', '10-charge', 33 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-034', 'DCF1','订仓费', '', '', '', '10-charge', 34 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-035', 'DCF2','堆场费', '', '', '', '10-charge', 35 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-036', 'DDC','DDC', '', '', '', '10-charge', 36 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-037', 'DDF','打单费', '', '', '', '10-charge', 37 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-038', 'DDSXF','代垫手续费', '', '', '', '10-charge', 38 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-039', 'DFF','电放费', '', '', '', '10-charge', 39 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-040', 'DJF','吊机费', '', '', '', '10-charge', 40 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-041', 'DJO','动检场地费', '', '', '', '10-charge', 41 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-042', 'DLF','代理费', '', '', '', '10-charge', 42 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-043', 'DLT','电脑套件', '', '', '', '10-charge', 43 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-044', 'DOC','DOC FEE', '', '', '', '10-charge', 44 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-045', 'DSF','待时费', '', '', '', '10-charge', 45 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-046', 'DTF','打托费', '', '', '', '10-charge', 46 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-047', 'DXF','倒箱费', '', '', '', '10-charge', 47 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-048', 'DZCLF','单证处理费', '', '', '', '10-charge', 48 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-049', 'DZJ','动植检', '', '', '', '10-charge', 49 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-050', 'EDI','EDI付税', '', '', '', '10-charge', 50 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-051', 'FBF','分拨费', '', '', '', '10-charge', 51 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-052', 'FDF','放单费', '', '', '', '10-charge', 52 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-053', 'FDF1','分单费', '', '', '', '10-charge', 53 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-054', 'FJF','附加费', '', '', '', '10-charge', 54 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-055', 'FJF1','返还附加费', '', '', '', '10-charge', 55 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-056', 'FKF','放空费', '', '', '', '10-charge', 56 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-057', 'FWF','服务费', '', '', '', '10-charge', 57 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-058', 'GDF','改单费', '', '', '', '10-charge', 58 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-059', 'GGF','更改费', '', '', '', '10-charge', 59 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-060', 'GGF1','公估费', '', '', '', '10-charge', 60 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-061', 'GJF','港建费', '', '', '', '10-charge', 61 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-062', 'GJTX','国际通信费', '', '', '', '10-charge', 62 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-063', 'GKBA','港口保安费', '', '', '', '10-charge', 63 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-064', 'GKCZ','港口操作费', '', '', '', '10-charge', 64 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-065', 'GS','关税', '', '', '', '10-charge', 65 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-066', 'GZF','港杂费', '', '', '', '10-charge', 66 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-067', 'H/C','Handling Charge(H/C)', '', '', '', '10-charge', 67 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-068', 'HANDLING','HANGLING CHARGE', '', '', '', '10-charge', 68 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-069', 'HCF','核查费', '', '', '', '10-charge', 69 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-070', 'HDF','换单费', '', '', '', '10-charge', 70 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-071', 'HGCY','海关查验交通费', '', '', '', '10-charge', 71 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-072', 'HGJB','海关加班费', '', '', '', '10-charge', 72 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-073', 'HGSBF','海关申报费', '', '', '', '10-charge', 73 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-074', 'HWBYF','货物搬运费', '', '', '', '10-charge', 74 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-075', 'HYF','海运费', '', '', '', '10-charge', 75 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-076', 'JDF','寄单费', '', '', '', '10-charge',76 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-077', 'JGCYF','监管车运费', '', '', '', '10-charge', 77 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-078', 'JGF','监管费', '', '', '', '10-charge', 78 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-079', 'JJB','加急报关费', '', '', '', '10-charge', 79 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-080', 'JJBG','紧急报关费', '', '', '', '10-charge', 80 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-081', 'JJF1','加急费', '', '', '', '10-charge', 81 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-082', 'JJF2','交际费', '', '', '', '10-charge', 82 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-083', 'JKBG','进口报关费', '', '', '', '10-charge', 83 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-084', 'JKF','集卡费', '', '', '', '10-charge', 84 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-085', 'JKGS','进口关税', '', '', '', '10-charge', 85 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-086', 'JKXGYWDLF','进口相关业务代理费', '', '', '', '10-charge', 86 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-087', 'JPFGN','机票费(国内)', '', '', '', '10-charge', 87 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-088', 'JPFGW','机票费(国外)', '', '', '', '10-charge', 88 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-089', 'JWG','进外高桥费', '', '', '', '10-charge', 89 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-090', 'JXF','机械费', '', '', '', '10-charge', 90 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-091', 'JXF1','监箱费', '', '', '', '10-charge', 91 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-092', 'JYF','检疫费', '', '', '', '10-charge', 92 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-093', 'JYJYF','检验检疫费', '', '', '', '10-charge', 93 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-094', 'JZXBY','集装箱办运费', '', '', '', '10-charge', 94 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-095', 'JZXHXF','集装箱换箱费', '', '', '', '10-charge', 95 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-096', 'JZXYF','集装箱运费', '', '', '', '10-charge', 96 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-097', 'K/B','K/B', '', '', '', '10-charge', 97 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-098', 'KCF','亏舱费', '', '', '', '10-charge', 98 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-099', 'KCYF','卡车运费', '', '', '', '10-charge', 99 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-100', 'KDF','快递费', '', '', '', '10-charge', 100 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-101', 'KJF','快件费', '', '', '', '10-charge', 101 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-102', 'KPF','改匹费', '', '', '', '10-charge', 102 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-103', 'KXCL','空箱处理费', '', '', '', '10-charge', 103 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-104', 'KYF','空运费', '', '', '', '10-charge', 104 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-105', 'LDF','拉单费', '', '', '', '10-charge', 105 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-106', 'LDF1','联单费', '', '', '', '10-charge', 106 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-107', 'LHF','理货费', '', '', '', '10-charge', 107 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-108', 'LPF','礼品费', '', '', '', '10-charge', 108 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-109', 'LWF','劳务费', '', '', '', '10-charge', 109 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-110', 'LXF','落箱费', '', '', '', '10-charge', 110 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-111', 'NTF','内托费', '', '', '', '10-charge', 111 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-112', 'NZXF','内装箱费', '', '', '', '10-charge', 112 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-113', 'PCYF','拼车运费', '', '', '', '10-charge', 113 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-114', 'PDF','跑单费', '', '', '', '10-charge', 114 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-115', 'PUC','Pick Up Charge(PUC)', '', '', '', '10-charge', 115 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-116', 'QDF','汽代费', '', '', '', '10-charge', 116 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-117', 'QGF','清关费', '', '', '', '10-charge', 117 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-118', 'QT','其他', '', '', '', '10-charge', 118 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-119', 'QTBGF','其他报关费', '', '', '', '10-charge', 119 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-120', 'QTCC','其他仓储费', '', '', '', '10-charge', 120 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-121', 'QTCY','其他查验费', '', '', '', '10-charge', 121 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-122', 'RBFY','日本费用', '', '', '', '10-charge', 122 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-123', 'RBFYSJ','日本费用税金', '', '', '', '10-charge', 123 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-124', 'SCT','SCT', '', '', '', '10-charge', 124 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-125', 'SDF','输单费', '', '', '', '10-charge', 125 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-126', 'SDF1','删单费', '', '', '', '10-charge', 126 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-127', 'SGF','疏港费', '', '', '', '10-charge', 127 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-128', 'SJ','税金', '', '', '', '10-charge', 128 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-129', 'SJCYF','商检查验费', '', '', '', '10-charge', 129 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-130', 'SJF1','三检费', '', '', '', '10-charge', 130 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-131', 'SJF2','商检费', '', '', '', '10-charge', 131 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-132', 'SJH','商检换单费', '', '', '', '10-charge', 132 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-133', 'SOC','Teminal Charge(SOC)', '', '', '', '10-charge', 133 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-134', 'SQF','速遣费', '', '', '', '10-charge', 134 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-135', 'SXF','上下车费', '', '', '', '10-charge', 135 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-136', 'SXF1','手续费', '', '', '', '10-charge', 136 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-137', 'T/F','Trucking Fee', '', '', '', '10-charge', 137 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-138', 'TBZK','特别折扣', '', '', '', '10-charge', 138 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-139', 'TGF','退关费', '', '', '', '10-charge', 139 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-140', 'THC','THC', '', '', '', '10-charge', 140 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-141', 'THF','提货费', '', '', '', '10-charge', 141 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-142', 'TM','贴唛', '', '', '', '10-charge', 142 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-143', 'TXCY','掏箱查验费', '', '', '', '10-charge', 143 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-144', 'TXF1','提箱费', '', '', '', '10-charge', 144 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-145', 'TXF2','掏箱费', '', '', '', '10-charge', 145 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-146', 'TXF3','拖箱费', '', '', '', '10-charge', 146 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-147', 'TY','退佣', '', '', '', '10-charge', 147 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-148', 'WJF','危检费', '', '', '', '10-charge', 148 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-149', 'WJF1','卫检费', '', '', '', '10-charge', 149 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-150', 'WJF2','文件费', '', '', '', '10-charge', 150 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-151', 'WLGLF','物流管理费', '', '', '', '10-charge', 151 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-152', 'WXPBZ','危险品标志', '', '', '', '10-charge', 152 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-153', 'WXPSBF','危险品申报费', '', '', '', '10-charge', 153 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-154', 'XDF','消毒费', '', '', '', '10-charge', 154 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-155', 'XFS','消费税', '', '', '', '10-charge', 155 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-156', 'XK','箱扣', '', '', '', '10-charge', 156 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-157', 'XTSY','系统使用费', '', '', '', '10-charge', 157 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-158', 'XXDCF','信息调查费', '', '', '', '10-charge', 158 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-159', 'XXF','保险费', '', '', '', '10-charge', 159 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-160', 'XXF1','洗箱费', '', '', '', '10-charge', 160 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-161', 'XZF','熏蒸费', '', '', '', '10-charge', 161 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-162', 'YJ','佣金', '', '', '', '10-charge', 162 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-163', 'YJF','邮寄费', '', '', '', '10-charge', 163 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-164', 'YLF','预录费', '', '', '', '10-charge', 164 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-165', 'YSF','运输费', '', '', '', '10-charge', 165 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-166', 'YWXZF','业务协助费', '', '', '', '10-charge', 166 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-167', 'YXF','移箱费', '', '', '', '10-charge', 167 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-168', 'YZF1','预支出车费', '', '', '', '10-charge', 168 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-169', 'YZF2','运杂费', '', '', '', '10-charge', 169 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-170', 'ZBJ','滞报金', '', '', '', '10-charge', 170 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-171', 'ZCF','装船费', '', '', '', '10-charge', 171 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-172', 'ZDF','制单费', '', '', '', '10-charge', 172 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-173', 'ZGBG','转关报关费', '', '', '', '10-charge', 173 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-174', 'ZSF','住宿费', '', '', '', '10-charge', 174 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-175', 'ZXF','装箱费', '', '', '', '10-charge', 175 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-176', 'ZXF1','装卸费', '', '', '', '10-charge', 176 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-177', 'ZXF2','注销费', '', '', '', '10-charge', 177 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-178', 'ZXF3','咨询费', '', '', '', '10-charge', 178 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-179', 'ZXF4','滞箱费', '', '', '', '10-charge', 179 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-180', 'ZXJ','重箱进港费', '', '', '', '10-charge', 180 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-181', 'ZZF','滞箱费', '', '', '', '10-charge', 181 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-182', 'ZZS','增值税', '', '', '', '10-charge', 182 ); + -- 贸易条款 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-01', 'GCJH', '工厂交货', '', '', '', '11-trade-clause', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-02', 'HJCYR', '货交承运人', '', '', '', '11-trade-clause', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-03', 'CBJH', '船边交货', '', '', '', '11-trade-clause', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-04', 'CSJH', '船上交货', '', '', '', '11-trade-clause', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-05', 'CBJYF', '成本加运费', '', '', '', '11-trade-clause', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-06', 'BXFJYF', '成本、保险费加运费', '', '', '', '11-trade-clause', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-07', 'YFFZ', '运费付至', '', '', '', '11-trade-clause', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-08', 'BXFFZ', '运费及保险费付至', '', '', '', '11-trade-clause', 8 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-09', 'BJJH', '边境交货', '', '', '', '11-trade-clause', 9 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-10', 'MDGCSJH', '目的港船上交货', '', '', '', '11-trade-clause', 10 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-11', 'MDGMTJH', '目的港码头交货', '', '', '', '11-trade-clause', 11 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-12', 'WWSJH', '未完税交货', '', '', '', '11-trade-clause', 12 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '11-trade-clause-13', 'WSHJH', '完税后交货', '', '', '', '11-trade-clause', 13 ); + -- 运费条款 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '12-freight-clause-1', 'PP','预付', '', '', '', '12-freight-clause', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '12-freight-clause-2', 'RP','到付', '', '', '', '12-freight-clause', 2 ); + -- 附加费条款 + -- 来往单位类型 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-bgh', 'BGH','报关行', '', '', '', '20-department', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-dls', 'DLS','代理商', '', '', '', '20-department', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-gys', 'GYS','供应商', '', '', '', '20-department', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-cd', 'CD','船代', '', '', '', '20-department', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-cyr', 'CYR','承运人', '', '', '', '20-department', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-chedui', 'CHEDUI','车队', '', '', '', '20-department', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-gwdl', 'GWDL','国外代理', '', '', '', '20-department', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-hkgs', 'HKGS','航空公司', '', '', '', '20-department', 8 ); + -- 国家地区 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-01', 'CN','中国', 'China', '', '', '30-state', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-02', 'JP','日本', 'Japan', '', '', '30-state', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-03', 'HK','香港', 'HongKong', '', '', '30-state', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-04', 'TW','台湾', 'TaiWan', '', '', '30-state', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-05', 'US','美国', 'American', '', '', '30-state', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-06', 'UK','英国', 'Britain', '', '', '30-state', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-07', 'AUS','澳大利亚', '', '', '', '30-state', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-08', 'CANADA','加拿大', '', '', '', '30-state', 8 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-09', 'FINLAND','芬兰', '', '', '', '30-state', 9 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-10', 'GERMANY','德国', '', '', '', '30-state', 10 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-11', 'INDIA','印度', '', '', '', '30-state', 11 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-12', 'INDONESIA','印度尼西亚本', '', '', '', '30-state', 12 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-13', 'MALAYSIA','马来西亚', '', '', '', '30-state', 13 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-14', 'PORTUGAL','葡萄牙', '', '', '', '30-state', 14 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-15', 'SINGAPORE','新加坡', '', '', '', '30-state', 15 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-16', 'SOUTH AFRICA','南非', '', '', '', '30-state', 16 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-17', 'SWEDEN','瑞典', '', '', '', '30-state', 17 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-18', 'TG','泰国', 'TAILAND', '', '', '30-state', 18 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-19', 'ZIMBABWE','津巴布韦', '', '', '', '30-state', 19 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-20', 'SYDNEY','悉尼', '', '', '', '30-state', 20 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-21', 'OSAKA','大阪', '', '', '', '30-state', 21 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-22', 'GZ','广州', '', '', '', '30-state', 22 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-23', 'NB','宁波', '', '', '', '30-state', 23 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '30-state-24', 'SH','上海', '', '', '', '30-state', 24 ); + -- 海运港口 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-01', 'HangZhou','杭州', '', '', '', '31-port-sea', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-02', 'ShangHai','上海', '', '', '', '31-port-sea', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-03', 'GuangZhou','广州', '', '', '', '31-port-sea', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-04', 'XiaMen','厦门', '', '', '', '31-port-sea', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-05', 'XiaoShan','萧山', '', '', '', '31-port-sea', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-06', 'HongKong','香港', '', '', '', '31-port-sea', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-07', 'Ningbo','宁波', '', '', '', '31-port-sea', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-08', 'Brisbane','布里斯班', '', '', '', '31-port-sea', 8 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-09', 'Buenos Aires','布宜诺斯艾利斯', '', '', '', '31-port-sea', 9 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-10', 'ChenNai','钦奈', '', '', '', '31-port-sea', 10 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-11', 'FelixStowe','弗里克斯托', '', '', '', '31-port-sea', 11 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-12', 'GothenBurg','歌德堡', '', '', '', '31-port-sea', 12 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-13', 'Hakata','博多', '', '', '', '31-port-sea', 13 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-14', 'Hambure','汉堡', '', '', '', '31-port-sea', 14 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-15', 'Harare','哈拉雷', '', '', '', '31-port-sea', 15 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-16', 'Hiroshima','广岛', '', '', '', '31-port-sea', 16 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-17', 'Imabari','今治', '', '', '', '31-port-sea', 17 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-18', 'Inchon','仁川', '', '', '', '31-port-sea', 18 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-19', 'JY','江阴', '', '', '', '31-port-sea', 19 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-20', 'Kanazawa','金泽', '', '', '', '31-port-sea', 20 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-21', 'Kobe','神户', '', '', '', '31-port-sea', 21 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-22', 'Kotka','Kotka', '', '', '', '31-port-sea', 22 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-23', 'Kumamoto','熊本', '', '', '', '31-port-sea', 23 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-24', 'Leixoes','莱特索斯', '', '', '', '31-port-sea', 24 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-25', 'Lisbon','里斯本', '', '', '', '31-port-sea', 25 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-26', 'Long Beach','长滩', '', '', '', '31-port-sea', 26 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-27', 'Los Angle','洛杉矶', '', '', '', '31-port-sea', 27 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-28', 'MG','曼谷', '', '', '', '32-port-air', 28 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-29', 'Mizushima','水岛', '', '', '', '32-port-air', 29 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-30', 'Moji','门司', '', '', '', '31-port-sea', 30 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-31', 'Montreal','蒙特利尔', '', '', '', '31-port-sea', 31 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-32', 'Mumbai','孟买', '', '', '', '31-port-sea', 32 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-33', 'Nagoya','名古屋', '', '', '', '31-port-sea', 33 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-34', 'Nagoya','Nagoya', '', '', '', '31-port-sea', 34 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-35', 'Nashville','纳什维尔', '', '', '', '31-port-sea', 35 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-36', 'New York','纽约', '', '', '', '31-port-sea', 36 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-37', 'Nhava Sheva','那瓦什瓦', '', '', '', '31-port-sea', 37 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-38', 'Niigata','新泻', '', '', '', '32-port-air', 38 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-39', 'Osaka','大阪', '', '', '', '32-port-air', 39 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-40', 'QingDao','青岛', '', '', '', '31-port-sea', 40 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-41', 'Rotterdam','鹿特丹', '', '', '', '31-port-sea', 41 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-42', 'ShenZhen','深圳', '', '', '', '31-port-sea', 42 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-43', 'Shimizu','清水', '', '', '', '31-port-sea', 43 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-44', 'SuZhou','苏州', '', '', '', '31-port-sea', 44 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-45', 'Singapore','新加坡', '', '', '', '31-port-sea', 45 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-46', 'Stockholm','斯德哥尔摩', '', '', '', '31-port-sea', 46 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-47', 'Sydney','悉尼', '', '', '', '31-port-sea', 47 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-48', 'Tokyo','东京', '', '', '', '32-port-air', 48 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-49', 'Toyama','富山', '', '', '', '32-port-air', 49 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-50', 'Tunis','突尼斯', '', '', '', '31-port-sea', 50 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-51', 'Vancouver','温哥华', '', '', '', '31-port-sea', 51 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-52', 'Zurich','苏黎世', '', '', '', '31-port-sea', 52 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-53', 'Xias','下沙', '', '', '', '31-port-sea', 53 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-54', 'TianJin','天津', '', '', '', '31-port-sea', 54 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-55', 'YanTai','烟台', '', '', '', '31-port-sea', 55 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-56', 'YanTian','盐田', '', '', '', '31-port-sea', 56 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-57', 'Yokkaichi','四日市', '', '', '', '31-port-sea', 57 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '31-port-sea-58', 'Yokohama','横滨', '', '', '', '32-port-air', 58 ); + -- 空运港口 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-01', 'BeiJing','北京', '', '', '', '32-port-air', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-02', 'ShangHai','上海', '', '', '', '32-port-air', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-03', 'GuangZhou','广州', '', '', '', '32-port-air', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-04', 'HangZhou','杭州', '', '', '', '32-port-air', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-05', 'HET','呼和浩特', '', '', '', '32-port-air', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-06', 'HongKong','香港', '', '', '', '32-port-air', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-07', 'ChangChun','长春', '', '', '', '32-port-air', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-08', 'BKK','曼谷', '', '', '', '32-port-air', 8 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-09', 'BAKU','巴库', '', '', '', '32-port-air', 9 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-10', 'INCHEON','仁川', '', '', '', '32-port-air', 10 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-11', 'FUKUOKA','福冈', '', '', '', '32-port-air', 11 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-12', 'Kuala Lumpur','吉隆坡', '', '', '', '32-port-air', 12 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-13', 'LA','洛杉矶', '', '', '', '32-port-air', 13 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-14', 'London','伦敦', '', '', '', '32-port-air', 14 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-15', 'Madrid','马德里', '', '', '', '32-port-air', 15 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-16', 'Mumbai','孟买', '', '', '', '32-port-air', 16 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-17', 'Nagoya','名古屋', '', '', '', '32-port-air', 17 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-18', 'NY','纽约', '', '', '', '32-port-air', 18 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-19', 'Niigata','新泻', '', '', '', '32-port-air', 19 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-20', 'Osaka','大阪', '', '', '', '32-port-air', 20 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-21', 'Paris','巴黎', '', '', '', '32-port-air', 21 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-22', 'Pusan','釜山', '', '', '', '32-port-air', 22 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-23', 'Singapore','新加坡', '', '', '', '32-port-air', 23 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-24', 'Stockholm','斯德哥尔摩', '', '', '', '32-port-air', 24 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-25', 'Tokyo','东京', '', '', '', '32-port-air', 25 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-26', 'XiaMen','厦门', '', '', '', '32-port-air', 26 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-27', 'American','美国', '', '', '', '32-port-air', 27 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '32-port-air-28', 'MAA','CHENAAI', '', '', '', '32-port-air', 28 ); + -- 海运航线 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-01', 'SH2NB','上海 —— 宁波', '', '沿海航线', '', '33-route-sea', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-02', 'NB2SH','宁波 —— 上海', '', '沿海航线', '', '33-route-sea', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-03', 'SH2GZ','上海 —— 广州', '', '沿海航线', '', '33-route-sea', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-04', 'GZ2SH','广州 —— 上海', '', '沿海航线', '', '33-route-sea', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-05', 'CN2JP','中国 —— 日本', '', '近洋航线', '', '33-route-sea', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-06', 'JP2CN','日本 —— 中国', '', '近洋航线', '', '33-route-sea', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-07', 'JPHX','日本航线', '', '近洋航线', '', '33-route-sea', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-08', 'CN2TG','中国 —— 泰国', '', '', '', '33-route-sea', 8 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-09', 'TG2CN','泰国 —— 中国', '', '', '', '33-route-sea', 9 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-10', 'FLBHX','菲律宾', '', '沿海航线', '', '33-route-sea', 10 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-11', 'GNYS','国内运输', '', '', '', '33-route-sea', 11 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-12', 'US2JP','美国 —— 日本', '', '远洋航线', '', '33-route-sea', 12 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-13', 'JP2US','日本 —— 美国', '', '远洋航线', '', '33-route-sea', 13 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-14', 'US2CN','美国 —— 中国', '', '远洋航线', '', '33-route-sea', 14 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '33-route-sea-15', 'CN2US','中国 —— 美国', '', '远洋航线', '', '33-route-sea', 15 ); + -- 空运航线 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '34-route-air-1', 'AUS2CN','澳大利亚 —— 中国', '', '', '', '34-route-air', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '34-route-air-2', 'CN2AUS','中国 —— 澳大利亚', '', '', '', '34-route-air', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '34-route-air-3', 'JPN2CN','日本 —— 中国', '', '', '', '34-route-air', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '34-route-air-4', 'CN2JPN','中国 —— 日本', '', '', '', '34-route-air', 4 ); + + +-- 创建表 - 合作伙伴数据 +create table KSA_BD_PARTNER ( + ID varchar(36) not null comment '标识' , + CODE varchar(200) not null comment '编码' , + NAME varchar(2000) not null comment '名称' , + ALIAS varchar(2000) not null comment '别名 - 主要显示在提单信息中' , + ADDRESS varchar(2000) not null comment '地址' , + PP int not null comment '付款周期 ( 天 )' , + NOTE varchar(2000) not null comment '备注' , + IMPORTANT int(1) default 0 not null comment '是否为重要伙伴' , + RANK int not null comment '排序' , + SALER_ID varchar(36) not null comment '销售担当标识', + primary key ( ID ), + unique ( CODE ) +); + + insert into KSA_BD_PARTNER ( ID, CODE, NAME, ALIAS, ADDRESS, PP, NOTE, IMPORTANT, RANK, SALER_ID ) values ( 'test-partner-1', 'test-partner-1', '测试合作伙伴1', 'Alias1-1', '', 30, '', 1, 1, 'test-user-1'); + insert into KSA_BD_PARTNER ( ID, CODE, NAME, ALIAS, ADDRESS, PP, NOTE, IMPORTANT, RANK, SALER_ID ) values ( 'test-partner-2', 'test-partner-2', '测试合作伙伴2', 'Alias2-1', '', 30, '', 0, 1001, '' ); + + +-- 创建表 - 合作伙伴附加提单信息 +create table KSA_BD_PARTNER_EXTRA ( + PARTNER_ID varchar(36) not null comment '合作伙伴标识' , + EXTRA varchar(2000) not null comment '附加提单信息' , + -- PARTNER_ID 外键关联合作伙伴表 + constraint FK_KSA_BD_PARTNER_EXTRA + foreign key ( PARTNER_ID ) + references KSA_BD_PARTNER ( ID ) + on delete cascade + on update cascade +); + + insert into KSA_BD_PARTNER_EXTRA ( PARTNER_ID, EXTRA ) values ( 'test-partner-1', 'extra1-1'); + insert into KSA_BD_PARTNER_EXTRA ( PARTNER_ID, EXTRA ) values ( 'test-partner-1', 'extra1-2'); + insert into KSA_BD_PARTNER_EXTRA ( PARTNER_ID, EXTRA ) values ( 'test-partner-2', 'extra2-1'); + +-- 创建表 - 合作伙伴单位类型信息 +create table KSA_BD_PARTNER_TYPE ( + PARTNER_ID varchar(36) not null comment '合作伙伴标识' , + TYPE_ID varchar(36) not null comment '单位类型标识' , + primary key (PARTNER_ID, TYPE_ID) , + + -- PARTNER_ID 外键关联合作伙伴表 + constraint FK_KSA_BD_PARTNER_TYPE1 + foreign key ( PARTNER_ID ) + references KSA_BD_PARTNER ( ID ) + on delete cascade + on update cascade, + + constraint FK_KSA_BD_PARTNER_TYPE2 + foreign key ( TYPE_ID ) + references KSA_BD_DATA ( ID ) + on delete cascade + on update cascade +); + + insert into KSA_BD_PARTNER_TYPE ( PARTNER_ID, TYPE_ID ) values ( 'test-partner-1', '20-department-bgh'); + insert into KSA_BD_PARTNER_TYPE ( PARTNER_ID, TYPE_ID ) values ( 'test-partner-2', '20-department-dls'); + insert into KSA_BD_PARTNER_TYPE ( PARTNER_ID, TYPE_ID ) values ( 'test-partner-2', '20-department-gys'); + +-- 创建表 - 汇率表 : 按日期记录 +create table KSA_BD_CURRENCY_RATE_BYDATE ( + ID varchar(36) not null comment '汇率标识', + CURRENCY_ID varchar(36) not null comment '关联货币的标识', + MONTH date not null comment '记录汇率的日期', + RATE numeric(10,5) not null comment '汇率值', + primary key ( ID ), + -- 外键关联汇率表 + constraint FK_KSA_BD_CURRENCY_RATE_BYDATE + foreign key ( CURRENCY_ID ) + references KSA_BD_DATA ( ID ) + on delete cascade + on update cascade +); + +-- 创建表 - 汇率表 : 按客户记录 +create table KSA_BD_CURRENCY_RATE_BYPARTNER ( + ID varchar(36) not null comment '汇率标识', + CURRENCY_ID varchar(36) not null comment '关联货币的标识', + PARTNER_ID varchar(36) not null comment '关联客户的标识', + RATE numeric(10,5) not null comment '汇率值', + primary key ( ID ), + -- 外键关联汇率表 + constraint FK_KSA_BD_CURRENCY_RATE_BYPARTNER + foreign key ( CURRENCY_ID ) + references KSA_BD_DATA ( ID ) + on delete cascade + on update cascade, + -- 外键关联客户表 + constraint FK_KSA_BD_CURRENCY_RATE_BYPARTNER2 + foreign key ( PARTNER_ID ) + references KSA_BD_PARTNER ( ID ) + on delete cascade + on update cascade +); \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/pom.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/pom.xml new file mode 100644 index 0000000..145e7b8 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + + com.ksa + ksa-dao-root + 3.9.0 + + + ksa-finance-dao + jar + + ksa-finance-dao + 杭州凯思爱物流管理系统 - 财务管理 DAO 模块 + + + UTF-8 + + + + + com.ksa + ksa-bd-dao + ${project.version} + + + diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDao.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDao.java new file mode 100644 index 0000000..f275762 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDao.java @@ -0,0 +1,37 @@ +package com.ksa.dao.finance.mybatis; + +import java.util.List; + +import com.ksa.dao.bd.mybatis.MybatisCurrencyRateDao; +import com.ksa.dao.finance.AccountCurrencyRateDao; +import com.ksa.model.finance.AccountCurrencyRate; + + +public class MybatisAccountCurrencyRateDao extends MybatisCurrencyRateDao implements AccountCurrencyRateDao { + + @Override + public int insertRate( AccountCurrencyRate rate ) throws RuntimeException { + return this.session.insert( "insert-finance-account-rate", rate ); + } + + @Override + public int updateRate( AccountCurrencyRate rate ) throws RuntimeException { + return this.session.update( "update-finance-account-rate", rate ); + } + + @Override + public int deleteRate( AccountCurrencyRate rate ) throws RuntimeException { + return this.session.delete( "delete-finance-account-rate", rate ); + } + + @Override + public List selectRatesByAccountId( String accountId ) throws RuntimeException { + return this.session.selectList( "select-finance-account-rates", accountId ); + } + + @Override + public AccountCurrencyRate selectRateById( String id ) throws RuntimeException { + return this.session.selectOne( "select-finance-account-rate-byid", id ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountDao.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountDao.java new file mode 100644 index 0000000..54c8d14 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisAccountDao.java @@ -0,0 +1,79 @@ +package com.ksa.dao.finance.mybatis; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.finance.AccountDao; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; + +/** + * 基于 Mybaits 的 AccountDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisAccountDao extends AbstractMybatisDao implements AccountDao { + + @Override + public int insertAccount( Account account ) throws RuntimeException { + return this.session.insert( "insert-finance-account", account ); + } + + + @Override + public int insertAccountCharges( Account account, List charges ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "accountId", account.getId() ); + paras.put( "charges", charges ); + return this.session.update( "update-finance-account-charges", paras ); + } + + @Override + public int deleteAccountCharges( Account account, List charges ) throws RuntimeException { + Map paras = new HashMap(); + paras.put( "accountId", null ); + paras.put( "charges", charges ); + return this.session.update( "update-finance-account-charges", paras ); + } + + @Override + public int updateAccount( Account account ) throws RuntimeException { + return this.session.update( "update-finance-account", account ); + } + + @Override + public int updateAccountState( Account account ) throws RuntimeException { + return this.session.update( "update-finance-account-state", account ); + } + + @Override + public int deleteAccount( Account account ) throws RuntimeException { + return this.session.delete( "delete-finance-account", account ); + } + + @Override + public Account selectAccountById( String id ) throws RuntimeException { + return this.session.selectOne( "select-finance-account-byid", id ); + } + + @Override + public List selectBookingNoteByAccountId( String accountId ) throws RuntimeException { + return this.session.selectList( "select-finance-bookingnote-byaccount", accountId ); + } + + + @Override + public int querySimilarAccountCodeCount( String code ) throws RuntimeException { + Map para = new HashMap(); + para.put( "code", code ); + Integer count = this.session.selectOne( "select-finance-account-similar-code-count", para ); + return count.intValue(); + } + + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisChargeDao.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisChargeDao.java new file mode 100644 index 0000000..17fd223 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisChargeDao.java @@ -0,0 +1,71 @@ +package com.ksa.dao.finance.mybatis; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.finance.ChargeDao; +import com.ksa.model.finance.Charge; +import com.ksa.model.finance.FinanceModel; + +/** + * 基于 Mybaits 的 ChargeDaoDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisChargeDao extends AbstractMybatisDao implements ChargeDao { + + protected String tableName; + + @Override + public int insertCharge( Charge charge ) throws RuntimeException { + return this.session.insert( "insert-finance-charge", charge ); + } + + @Override + public int updateCharge( Charge charge ) throws RuntimeException { + return this.session.update( "update-finance-charge", charge ); + } + + @Override + public int deleteCharge( Charge charge ) throws RuntimeException { + return this.session.delete( "delete-finance-charge", charge ); + } + + @Override + public Charge selectChargeById( String id ) throws RuntimeException { + return this.session.selectOne( "select-finance-charge-byid", id ); + } + + @Override + public List selectChargeByBookingNoteId( String id ) throws RuntimeException { + return selectChargeByBookingNoteId( id, 0, 0 ); + } + + @Override + public List selectChargeByBookingNoteId( String id, int direction, int nature ) throws RuntimeException { + Map param = new HashMap(); + param.put( "id", id ); + if( FinanceModel.isIncome( direction ) ) { + param.put( "direction", FinanceModel.INCOME ); + } else if( FinanceModel.isExpense( direction ) ) { + param.put( "direction", FinanceModel.EXPENSE ); + } + + if( FinanceModel.isNative( nature ) ) { + param.put( "nature", FinanceModel.NATIVE ); + } else if( FinanceModel.isForeign( nature ) ) { + param.put( "nature", FinanceModel.FOREIGN ); + } + + return this.session.selectList( "select-finance-charge-bybookingnote", param ); + } + + @Override + public List selectChargeByAccountId( String id ) throws RuntimeException { + return this.session.selectList( "select-finance-charge-byaccount", id ); + } +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDao.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDao.java new file mode 100644 index 0000000..1f97b8f --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDao.java @@ -0,0 +1,48 @@ +package com.ksa.dao.finance.mybatis; + +import java.util.List; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.finance.InvoiceDao; +import com.ksa.model.finance.Invoice; + +/** + * 基于 Mybaits 的 InvoiceDaoDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisInvoiceDao extends AbstractMybatisDao implements InvoiceDao { + + @Override + public int insertInvoice( Invoice invoice ) throws RuntimeException { + return this.session.insert( "insert-finance-invoice", invoice ); + } + + @Override + public int updateInvoice( Invoice invoice ) throws RuntimeException { + return this.session.update( "update-finance-invoice", invoice ); + } + + @Override + public int deleteInvoice( Invoice invoice ) throws RuntimeException { + return this.session.delete( "delete-finance-invoice", invoice ); + } + + @Override + public int updateInvoiceAccount( Invoice invoice ) throws RuntimeException { + return this.session.update( "update-finance-invoice-account", invoice ); + } + + @Override + public Invoice selectInvoiceById( String id ) throws RuntimeException { + return this.session.selectOne( "select-finance-invoice-byid", id ); + } + + @Override + public List selectInvoiceByAccountId( String id ) throws RuntimeException { + return this.session.selectList( "select-finance-invoice-byaccount", id ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-bookingnote.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-bookingnote.xml new file mode 100644 index 0000000..cd849d8 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-bookingnote.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-currency-rate.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-currency-rate.xml new file mode 100644 index 0000000..c2b228f --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account-currency-rate.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_FINANCE_CURRENCY_RATE_BYACCOUNT + ( ID, CURRENCY_ID, ACCOUNT_ID, RATE ) + VALUES ( #{id,jdbcType=VARCHAR}, #{currency.id,jdbcType=VARCHAR}, #{account.id,jdbcType=VARCHAR}, #{rate} ) + + + + UPDATE KSA_FINANCE_CURRENCY_RATE_BYACCOUNT SET + RATE = #{rate} + WHERE ID = #{id} + + + + DELETE FROM KSA_FINANCE_CURRENCY_RATE_BYACCOUNT WHERE ID = #{id} + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account.xml new file mode 100644 index 0000000..24c10fc --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-account.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_FINANCE_ACCOUNT + ( ID, CODE, TARGET_ID, CREATOR_ID, CREATED_DATE, DEADLINE, PAYMENT_DATE, NOTE, STATE, DIRECTION, NATURE ) + VALUES ( #{id}, #{code,jdbcType=VARCHAR}, #{target.id,jdbcType=VARCHAR}, #{creator.id,jdbcType=VARCHAR}, #{createdDate,jdbcType=DATE}, + #{deadline,jdbcType=DATE}, #{paymentDate,jdbcType=DATE}, #{note,jdbcType=VARCHAR}, #{state,jdbcType=NUMERIC}, + #{direction,jdbcType=NUMERIC}, #{nature,jdbcType=NUMERIC} ) + + + + UPDATE KSA_FINANCE_CHARGE SET + ACCOUNT_ID = #{accountId,jdbcType=VARCHAR} + WHERE ID IN ( + + #{charge.id} + + ) + + + + + UPDATE KSA_FINANCE_ACCOUNT SET + + TARGET_ID = #{target.id,jdbcType=VARCHAR}, + CREATOR_ID = #{creator.id,jdbcType=VARCHAR}, + CREATED_DATE = #{createdDate,jdbcType=DATE}, + DEADLINE = #{deadline,jdbcType=DATE}, + PAYMENT_DATE = #{paymentDate,jdbcType=DATE}, + NOTE = #{note,jdbcType=VARCHAR}, + DIRECTION = #{direction,jdbcType=NUMERIC}, + NATURE = #{nature,jdbcType=NUMERIC} + WHERE ID = #{id} + + + + + UPDATE KSA_FINANCE_ACCOUNT SET + STATE = #{state,jdbcType=NUMERIC} + WHERE ID = #{id} + + + + + DELETE FROM KSA_FINANCE_ACCOUNT WHERE ID = #{id} + + + + SELECT a.ID, a.CODE, a.CREATED_DATE, a.DEADLINE, a.PAYMENT_DATE, a.NOTE, a.STATE, a.DIRECTION,a.NATURE, + p.ID as TARGET_ID, p.NAME as TARGET_NAME, + u.ID as CREATOR_ID, u.NAME as CREATOR_NAME + FROM KSA_FINANCE_ACCOUNT a + LEFT JOIN KSA_BD_PARTNER p ON p.ID = a.TARGET_ID + LEFT JOIN KSA_SECURITY_USER u ON u.ID = a.CREATOR_ID + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-bookingnote.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-bookingnote.xml new file mode 100644 index 0000000..3ff08d3 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-bookingnote.xml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT bn.*, + c1.NAME AS CUSTOMER_NAME, + u1.NAME AS CREATOR_NAME, + u2.NAME AS SALER_NAME, + p1.NAME AS CARRIER_NAME, + p2.NAME AS SHIPPING_AGENT_NAME, + p3.NAME AS SHIPPER_NAME, p3.ALIAS AS SHIPPER_ALIAS, + p4.NAME AS CONSIGNEE_NAME, p4.ALIAS AS CONSIGNEE_ALIAS, + p5.NAME AS NOTIFY_NAME, p5.ALIAS AS NOTIFY_ALIAS, + s1.NAME AS AGENT_NAME, + s2.NAME AS CUSTOMS_BROKER_NAME, + s3.NAME AS VEHICLE_TEAM_NAME + FROM KSA_LOGISTICS_BOOKINGNOTE bn + LEFT JOIN KSA_BD_PARTNER c1 ON bn.CUSTOMER_ID = c1.ID + LEFT JOIN KSA_SECURITY_USER u1 ON bn.CREATOR_ID = u1.ID + LEFT JOIN KSA_SECURITY_USER u2 ON bn.SALER_ID = u2.ID + LEFT JOIN KSA_BD_PARTNER p1 ON bn.CARRIER_ID = p1.ID + LEFT JOIN KSA_BD_PARTNER p2 ON bn.SHIPPING_AGENT_ID = p2.ID + LEFT JOIN KSA_BD_PARTNER p3 ON bn.SHIPPER_ID = p3.ID + LEFT JOIN KSA_BD_PARTNER p4 ON bn.CONSIGNEE_ID = p4.ID + LEFT JOIN KSA_BD_PARTNER p5 ON bn.NOTIFY_ID = p5.ID + LEFT JOIN KSA_BD_PARTNER s1 ON bn.AGENT_ID = s1.ID + LEFT JOIN KSA_BD_PARTNER s2 ON bn.CUSTOMS_BROKER_ID = s2.ID + LEFT JOIN KSA_BD_PARTNER s3 ON bn.VEHICLE_TEAM_ID = s3.ID + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge-bookingnote.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge-bookingnote.xml new file mode 100644 index 0000000..ac23268 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge-bookingnote.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge.xml new file mode 100644 index 0000000..bf0d5bb --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-charge.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_FINANCE_CHARGE + ( ID, TARGET_ID, TYPE, CURRENCY_ID, PRICE, QUANTITY, AMOUNT, CREATED_DATE, NOTE, + CREATOR_ID, ACCOUNT_ID, BOOKINGNOTE_ID, DIRECTION, NATURE, RANK ) + VALUES ( #{id}, #{target.id,jdbcType=VARCHAR}, #{type,jdbcType=VARCHAR}, #{currency.id,jdbcType=VARCHAR}, + #{price,jdbcType=NUMERIC}, #{quantity,jdbcType=NUMERIC}, #{amount}, #{createdDate,jdbcType=DATE}, #{note,jdbcType=VARCHAR}, + #{creator.id,jdbcType=VARCHAR}, #{account.id,jdbcType=VARCHAR}, #{bookingNote.id,jdbcType=VARCHAR}, + #{direction,jdbcType=NUMERIC}, #{nature,jdbcType=NUMERIC}, #{rank,jdbcType=NUMERIC} ) + + + + + UPDATE KSA_FINANCE_CHARGE SET + TARGET_ID = #{target.id,jdbcType=VARCHAR}, + CREATOR_ID = #{creator.id,jdbcType=VARCHAR}, + TYPE = #{type,jdbcType=VARCHAR}, + CURRENCY_ID = #{currency.id,jdbcType=VARCHAR}, + PRICE = #{price,jdbcType=NUMERIC}, + QUANTITY = #{quantity,jdbcType=NUMERIC}, + AMOUNT = #{amount}, + NOTE = #{note,jdbcType=VARCHAR}, + ACCOUNT_ID = #{account.id,jdbcType=VARCHAR}, + DIRECTION = #{direction,jdbcType=NUMERIC}, + NATURE = #{nature,jdbcType=NUMERIC}, + RANK = #{rank,jdbcType=NUMERIC} + WHERE ID = #{id} + + + + + DELETE FROM KSA_FINANCE_CHARGE WHERE ID = #{id} + + + + SELECT c.*, + p.ID as TARGET_ID, p.NAME as TARGET_NAME, + d.ID as CURRENCY_ID, d.CODE as CURRENCY_CODE, d.NAME as CURRENCY_NAME, d.RANK as CURRENCY_RANK, + u.ID as CREATOR_ID, u.NAME as CREATOR_NAME, + a.STATE as ACCOUNT_STATE + FROM KSA_FINANCE_CHARGE c + LEFT JOIN KSA_BD_PARTNER p ON p.ID = c.TARGET_ID + LEFT JOIN KSA_BD_DATA d ON d.ID = c.CURRENCY_ID + LEFT JOIN KSA_SECURITY_USER u ON u.ID = c.CREATOR_ID + LEFT JOIN KSA_FINANCE_ACCOUNT a ON a.ID = c.ACCOUNT_ID + + + + + + + + + + + + + + SELECT c.*, + p.NAME as TARGET_NAME, + d.CODE as CURRENCY_CODE, d.NAME as CURRENCY_NAME, d.RANK as CURRENCY_RANK, + u.NAME as CREATOR_NAME, + a.STATE as ACCOUNT_STATE, + bn.SERIAL_NUMBER as BN_SERIAL_NUMBER, bn.CODE as BN_CODE, bn.TYPE as BN_TYPE, + bn.CREATED_DATE as BN_CREATED_DATE, bn.CHARGE_DATE as BN_CHARGE_DATE, bn.INVOICE_NUMBER as BN_INVOICE_NUMBER, + bn.CARGO_NAME as BN_CARGO_NAME, bn.DEPARTURE as BN_DEPARTURE, bn.DEPARTURE_PORT as BN_DEPARTURE_PORT, + bn.DEPARTURE_DATE as BN_DEPARTURE_DATE, bn.DESTINATION as BN_DESTINATION, bn.DESTINATION_PORT as BN_DESTINATION_PORT, + bn.DESTINATION_DATE as BN_DESTINATION_DATE, bn.STATE as BN_STATE + FROM KSA_FINANCE_CHARGE c + LEFT JOIN KSA_BD_PARTNER p ON p.ID = c.TARGET_ID + LEFT JOIN KSA_BD_DATA d ON d.ID = c.CURRENCY_ID + LEFT JOIN KSA_SECURITY_USER u ON u.ID = c.CREATOR_ID + LEFT JOIN KSA_FINANCE_ACCOUNT a ON a.ID = c.ACCOUNT_ID + LEFT JOIN KSA_LOGISTICS_BOOKINGNOTE bn ON bn.ID = c.BOOKINGNOTE_ID + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-invoice.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-invoice.xml new file mode 100644 index 0000000..5e344bf --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-invoice.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_FINANCE_INVOICE + ( ID, CODE, INVOICE_NUMBER, TAX_NUMBER, TYPE, AMOUNT, CREATED_DATE, NOTE, + CREATOR_ID, ACCOUNT_ID, CURRENCY_ID, TARGET_ID, DIRECTION, NATURE ) + VALUES ( #{id}, #{code,jdbcType=VARCHAR}, #{number,jdbcType=VARCHAR}, #{taxNumber,jdbcType=VARCHAR}, #{type,jdbcType=VARCHAR}, #{amount}, + #{createdDate,jdbcType=DATE}, #{note,jdbcType=VARCHAR}, + #{creator.id,jdbcType=VARCHAR}, #{account.id,jdbcType=VARCHAR}, #{currency.id,jdbcType=VARCHAR}, + #{target.id,jdbcType=VARCHAR}, #{direction,jdbcType=NUMERIC}, #{nature,jdbcType=NUMERIC} ) + + + + + UPDATE KSA_FINANCE_INVOICE SET + CODE = #{code,jdbcType=VARCHAR}, + INVOICE_NUMBER = #{number,jdbcType=VARCHAR}, + TAX_NUMBER = #{taxNumber,jdbcType=VARCHAR}, + TYPE = #{type,jdbcType=VARCHAR}, + AMOUNT = #{amount}, + CREATED_DATE = #{createdDate,jdbcType=DATE}, + NOTE = #{note,jdbcType=VARCHAR}, + CREATOR_ID = #{creator.id,jdbcType=VARCHAR}, + ACCOUNT_ID = #{account.id,jdbcType=VARCHAR}, + CURRENCY_ID = #{currency.id,jdbcType=VARCHAR}, + TARGET_ID = #{target.id,jdbcType=VARCHAR}, + DIRECTION = #{direction,jdbcType=NUMERIC}, + NATURE = #{nature,jdbcType=NUMERIC} + WHERE ID = #{id} + + + + + UPDATE KSA_FINANCE_INVOICE SET + ACCOUNT_ID = #{account.id,jdbcType=VARCHAR} + WHERE ID = #{id} + + + + + DELETE FROM KSA_FINANCE_INVOICE WHERE ID = #{id} + + + + SELECT i.*, + c.CODE as CURRENCY_CODE, c.NAME as CURRENCY_NAME, + p.NAME as TARGET_NAME, + u.NAME as CREATOR_NAME, + a.CODE as ACCOUNT_CODE, a.STATE as ACCOUNT_STATE + FROM KSA_FINANCE_INVOICE i + LEFT JOIN KSA_BD_DATA c ON c.ID = i.CURRENCY_ID + LEFT JOIN KSA_BD_PARTNER p ON p.ID = i.TARGET_ID + LEFT JOIN KSA_SECURITY_USER u ON u.ID = i.CREATOR_ID + LEFT JOIN KSA_FINANCE_ACCOUNT a ON a.ID = i.ACCOUNT_ID + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit-charge.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit-charge.xml new file mode 100644 index 0000000..79c305e --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit-charge.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit.xml new file mode 100644 index 0000000..fe05a54 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/mybatis/mapper/finance-profit.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT bn.*, + c1.NAME AS CUSTOMER_NAME, + u1.NAME AS CREATOR_NAME, + u2.NAME AS SALER_NAME, + p1.NAME AS SHIPPER_NAME, + p2.NAME AS CONSIGNEE_NAME, + p3.NAME AS CARRIER_NAME, + p4.NAME AS AGENT_NAME, + p5.NAME AS CUSTOMS_BROKER_NAME + FROM KSA_LOGISTICS_BOOKINGNOTE bn + LEFT JOIN KSA_BD_PARTNER c1 ON bn.CUSTOMER_ID = c1.ID + LEFT JOIN KSA_SECURITY_USER u1 ON bn.CREATOR_ID = u1.ID + LEFT JOIN KSA_SECURITY_USER u2 ON bn.SALER_ID = u2.ID + LEFT JOIN KSA_BD_PARTNER p1 ON bn.SHIPPER_ID = p1.ID + LEFT JOIN KSA_BD_PARTNER p2 ON bn.CONSIGNEE_ID = p2.ID + LEFT JOIN KSA_BD_PARTNER p3 ON bn.CARRIER_ID = p3.ID + LEFT JOIN KSA_BD_PARTNER p4 ON bn.AGENT_ID = p4.ID + LEFT JOIN KSA_BD_PARTNER p5 ON bn.CUSTOMS_BROKER_ID = p5.ID + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/spring/dao/finance-dao-context.xml b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/spring/dao/finance-dao-context.xml new file mode 100644 index 0000000..d9be1f6 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/main/resources/spring/dao/finance-dao-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDaoTest.java new file mode 100644 index 0000000..69a784d --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountCurrencyRateDaoTest.java @@ -0,0 +1,63 @@ +package com.ksa.dao.finance.mybatis; + +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.finance.AccountCurrencyRateDao; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.AccountCurrencyRate; + +public class MybatisAccountCurrencyRateDaoTest extends MybatisDaoTest { + + private static final Currency TEST_CURRENCY = new Currency(); + private static final Account TEST_ACCOUNT = new Account(); + static { + TEST_CURRENCY.setId( "00-currency-USD" ); + TEST_CURRENCY.setCode( "USD" ); + TEST_CURRENCY.setName( "美元" ); + TEST_CURRENCY.setExtra( "6.800" ); + + TEST_ACCOUNT.setId( "test-account-1" ); + TEST_ACCOUNT.setCode( "test-account-1" ); + } + + @Test + public void testCrudCurrencyRateByDate() throws RuntimeException { + AccountCurrencyRateDao dao = CONTEXT.getBean( "accountCurrencyRateDao", AccountCurrencyRateDao.class ); + + AccountCurrencyRate rate = new AccountCurrencyRate(); + String id = UUID.randomUUID().toString(); + rate.setId( id ); + rate.setCurrency( TEST_CURRENCY ); + rate.setRate( 123.456f ); + rate.setAccount( TEST_ACCOUNT ); + + + // 测试插入是否成功 + dao.insertRate( rate ); + AccountCurrencyRate temp = (AccountCurrencyRate) dao.selectRateById( id ); + Assert.assertEquals( 123.456f, temp.getRate(), 0.001 ); + Assert.assertEquals( TEST_CURRENCY.getId(), temp.getCurrency().getId() ); + Assert.assertEquals( TEST_ACCOUNT.getId(), temp.getAccount().getId() ); + + rate.setRate( 654.321f ); + // 测试更新是否成功 + Assert.assertEquals( 1, dao.updateRate( rate ) ); + temp = (AccountCurrencyRate) dao.selectRateById( id ); + Assert.assertNotNull( temp ); + Assert.assertEquals( 654.321f, temp.getRate(), 0.001 ); + + // 测试删除 + Assert.assertEquals( 1, dao.deleteRate( rate ) ); + CurrencyRate temp3 = dao.selectRateById( id ); + Assert.assertNull( temp3 ); + } + + + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountDaoTest.java new file mode 100644 index 0000000..afcd62b --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisAccountDaoTest.java @@ -0,0 +1,88 @@ +package com.ksa.dao.finance.mybatis; + +import java.util.Date; +import java.util.UUID; + +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.finance.AccountDao; +import com.ksa.model.bd.Partner; +import com.ksa.model.finance.Account; +import com.ksa.model.security.User; + +import junit.framework.Assert; + + +public class MybatisAccountDaoTest extends MybatisDaoTest { + + private static User TEST_USER1 = new User(); + private static User TEST_USER2 = new User(); + private static Partner TEST_PARTNER1 = new Partner(); + private static Partner TEST_PARTNER2 = new Partner(); + static { + TEST_USER1.setId( "test-user-1" ); + TEST_USER2.setId( "test-user-2" ); + TEST_PARTNER1.setId( "test-partner-1" ); + TEST_PARTNER2.setId( "test-partner-2" ); + + } + + @SuppressWarnings( "deprecation" ) + @Test + public void testCrudAccount() throws RuntimeException { + AccountDao dao = CONTEXT.getBean( "accountDao", AccountDao.class ); + + Account account = new Account(); + Date now = new Date( 2000, 1, 2); + String id = UUID.randomUUID().toString(); + account.setId( id ); + account.setCode( "code1" ); + account.setTarget( TEST_PARTNER1 ); + account.setCreator( TEST_USER1 ); + account.setCreatedDate( now ); + account.setDeadline( now ); + account.setPaymentDate( now ); + account.setNote( "note1" ); + account.setState( 1 ); + + // 测试插入是否成功 + dao.insertAccount( account ); + Account temp = dao.selectAccountById( id ); + Assert.assertEquals( "code1", temp.getCode() ); + Assert.assertEquals( TEST_USER1.getId(), temp.getCreator().getId() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getTarget().getId() ); + Assert.assertEquals( now, temp.getCreatedDate() ); + Assert.assertEquals( now, temp.getDeadline() ); + Assert.assertEquals( now, temp.getPaymentDate() ); + Assert.assertEquals( "note1", temp.getNote() ); + Assert.assertEquals( 1, temp.getState() ); + + now = new Date( 2012, 11, 20 ); + account.setCode( "code2" ); + account.setTarget( TEST_PARTNER2 ); + account.setCreator( TEST_USER2 ); + account.setCreatedDate( now ); + account.setDeadline( now ); + account.setPaymentDate( now ); + account.setNote( "note2" ); + account.setState( 2 ); + + // 测试更新是否成功 + dao.updateAccount( account ); + temp = dao.selectAccountById( id ); + Assert.assertEquals( "code1", temp.getCode() ); // code 不能修改 + Assert.assertEquals( TEST_USER2.getId(), temp.getCreator().getId() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getTarget().getId() ); + Assert.assertEquals( now, temp.getCreatedDate() ); + Assert.assertEquals( now, temp.getDeadline() ); + Assert.assertEquals( now, temp.getPaymentDate() ); + Assert.assertEquals( "note2", temp.getNote() ); + Assert.assertEquals( 1, temp.getState() ); // 状态单独修改 + + // 测试删除 + dao.deleteAccount( account ); + temp = dao.selectAccountById( id ); + Assert.assertNull( temp ); + } +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisChargeDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisChargeDaoTest.java new file mode 100644 index 0000000..d15e060 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisChargeDaoTest.java @@ -0,0 +1,106 @@ +package com.ksa.dao.finance.mybatis; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.UUID; + +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.finance.ChargeDao; +import com.ksa.model.bd.ChargeType; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.Partner; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.security.User; + +import junit.framework.Assert; + + +public class MybatisChargeDaoTest extends MybatisDaoTest { + + private static User TEST_USER = new User(); + private static Partner TEST_PARTNER1 = new Partner(); + private static Partner TEST_PARTNER2 = new Partner(); + private static BookingNote TEST_BN = new BookingNote(); + private static ChargeType TEST_CHARGE_TYPE1 = new ChargeType(); + private static ChargeType TEST_CHARGE_TYPE2 = new ChargeType(); + static { + TEST_PARTNER1.setId( "test-partner-1" ); + TEST_PARTNER2.setId( "test-partner-2" ); + TEST_USER.setId( "test-user-1" ); + TEST_BN.setId( "test-bookingnote-1" ); + TEST_CHARGE_TYPE1.setId( "10-charge-001" ); + TEST_CHARGE_TYPE2.setId( "10-charge-002" ); + } + + @SuppressWarnings( "deprecation" ) + @Test + public void testCrudCharge() throws RuntimeException { + ChargeDao dao = CONTEXT.getBean( "chargeDao", ChargeDao.class ); + + Charge charge = new Charge(); + String id = UUID.randomUUID().toString(); + Date now = new Date( 2000, 2, 2 ); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + charge.setId( id ); + charge.setDirection( 1 ); + charge.setTarget( TEST_PARTNER1 ); + charge.setPrice( 3.0f ); + charge.setQuantity( 2.0f ); + charge.setAmount( 6.0f ); + charge.setBookingNote( TEST_BN ); + charge.setCreator( TEST_USER ); + charge.setType( TEST_CHARGE_TYPE1.getId() ); + charge.setCurrency( Currency.USD ); + charge.setCreatedDate( now ); + charge.setNote( "note1" ); + + // 测试插入是否成功 + dao.insertCharge( charge ); + Charge temp = dao.selectChargeById( id ); + Assert.assertEquals( df.format( now ), df.format( temp.getCreatedDate() ) ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getTarget().getId() ); + Assert.assertEquals( 1, temp.getDirection() ); + Assert.assertEquals( 3.0f, temp.getPrice(), 0.001 ); + Assert.assertEquals( 2, temp.getQuantity().intValue() ); + Assert.assertEquals( 6.0f, temp.getAmount(), 0.001 ); + Assert.assertEquals( "note1", temp.getNote() ); + Assert.assertEquals( TEST_CHARGE_TYPE1.getId(), temp.getType() ); + Assert.assertEquals( TEST_USER.getId(), temp.getCreator().getId() ); + Assert.assertEquals( Currency.USD.getId(), temp.getCurrency().getId() ); + Assert.assertEquals( Currency.USD.getName(), temp.getCurrency().getName() ); + + Date now2 = new Date( 2011, 3, 3 ); + charge.setCreatedDate( now2 ); + charge.setDirection( -1 ); + charge.setTarget( TEST_PARTNER2 ); + charge.setPrice( 4.0f ); + charge.setQuantity( 3.0f ); + charge.setAmount( 12.0f ); + charge.setType( TEST_CHARGE_TYPE2.getId() ); + charge.setCurrency( Currency.RMB ); + charge.setNote( "note2" ); + + // 测试更新是否成功 + dao.updateCharge( charge ); + temp = dao.selectChargeById( id ); + Assert.assertEquals( df.format( now ), df.format( temp.getCreatedDate() ) ); // 创建时间不更新 + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getTarget().getId() ); + Assert.assertEquals( -1, temp.getDirection() ); + Assert.assertEquals( 4.0f, temp.getPrice(), 0.001 ); + Assert.assertEquals( 3, temp.getQuantity().intValue() ); + Assert.assertEquals( 12.0f, temp.getAmount(), 0.001 ); + Assert.assertEquals( "note2", temp.getNote() ); + Assert.assertEquals( TEST_CHARGE_TYPE2.getId(), temp.getType() ); + Assert.assertEquals( Currency.RMB.getId(), temp.getCurrency().getId() ); + Assert.assertEquals( Currency.RMB.getName(), temp.getCurrency().getName() ); + + // 测试删除 + dao.deleteCharge( charge ); + temp = dao.selectChargeById( id ); + Assert.assertNull( temp ); + } +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDaoTest.java new file mode 100644 index 0000000..83f790f --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/java/com/ksa/dao/finance/mybatis/MybatisInvoiceDaoTest.java @@ -0,0 +1,98 @@ +package com.ksa.dao.finance.mybatis; + +import java.util.Date; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.finance.InvoiceDao; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.Partner; +import com.ksa.model.finance.Invoice; +import com.ksa.model.security.User; + + +public class MybatisInvoiceDaoTest extends MybatisDaoTest { + + private static User TEST_USER1 = new User(); + private static User TEST_USER2 = new User(); + private static Partner TEST_PARTNER1 = new Partner(); + private static Partner TEST_PARTNER2 = new Partner(); + static { + TEST_USER1.setId( "test-user-1" ); + TEST_USER2.setId( "test-user-2" ); + TEST_PARTNER1.setId( "test-partner-1" ); + TEST_PARTNER2.setId( "test-partner-2" ); + + } + + @SuppressWarnings( "deprecation" ) + @Test + public void testCrudInvoice() throws RuntimeException { + InvoiceDao dao = CONTEXT.getBean( "invoiceDao", InvoiceDao.class ); + + Invoice invoice = new Invoice(); + String id = UUID.randomUUID().toString(); + Date now = new Date( 1999, 1, 1 ); + invoice.setId( id ); + invoice.setCode( "code1" ); + invoice.setNumber( "number1" ); + invoice.setTaxNumber( "taxNumber1" ); + invoice.setAmount( 1.0f ); + invoice.setType( "type1" ); + invoice.setNote( "note1" ); + invoice.setCreatedDate( now ); + invoice.setCreator( TEST_USER1 ); + invoice.setTarget( TEST_PARTNER1 ); + invoice.setCurrency( Currency.USD ); + + // 测试插入是否成功 + dao.insertInvoice( invoice ); + Invoice temp = dao.selectInvoiceById( id ); + Assert.assertEquals( 1.0f, temp.getAmount(), 0.001 ); + Assert.assertEquals( "code1", temp.getCode() ); + Assert.assertEquals( "number1", temp.getNumber() ); + Assert.assertEquals( "taxNumber1", temp.getTaxNumber() ); + Assert.assertEquals( "type1", temp.getType() ); + Assert.assertEquals( "note1", temp.getNote() ); + Assert.assertEquals( now, temp.getCreatedDate() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getTarget().getId() ); + Assert.assertEquals( TEST_USER1.getId(), temp.getCreator().getId() ); + Assert.assertEquals( Currency.USD.getId(), temp.getCurrency().getId() ); + + + now = new Date( 2000, 2, 2 ); + invoice.setCode( "code2" ); + invoice.setNumber( "number2" ); + invoice.setTaxNumber( "taxNumber2" ); + invoice.setAmount( 2.0f ); + invoice.setType( "type2" ); + invoice.setNote( "note2" ); + invoice.setCreatedDate( now ); + invoice.setCreator( TEST_USER2 ); + invoice.setTarget( TEST_PARTNER2 ); + invoice.setCurrency( Currency.RMB ); + + // 测试更新是否成功 + dao.updateInvoice( invoice ); + temp = dao.selectInvoiceById( id ); + Assert.assertEquals( 2.0f, temp.getAmount(), 0.001 ); + Assert.assertEquals( "code2", temp.getCode() ); + Assert.assertEquals( "number2", temp.getNumber() ); + Assert.assertEquals( "taxNumber2", temp.getTaxNumber() ); + Assert.assertEquals( "type2", temp.getType() ); + Assert.assertEquals( "note2", temp.getNote() ); + Assert.assertEquals( now, temp.getCreatedDate() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getTarget().getId() ); + Assert.assertEquals( TEST_USER2.getId(), temp.getCreator().getId() ); + Assert.assertEquals( Currency.RMB.getId(), temp.getCurrency().getId() ); + + // 测试删除 + dao.deleteInvoice( invoice ); + temp = dao.selectInvoiceById( id ); + Assert.assertNull( temp ); + } +} diff --git a/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/resources/init.sql b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/resources/init.sql new file mode 100644 index 0000000..625f454 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-finance-dao/src/test/resources/init.sql @@ -0,0 +1,220 @@ +-- ---------------------- 测试相关表及数据 ---------------------------- +create table KSA_LOGISTICS_BOOKINGNOTE ( + ID varchar(36) not null comment '标识' +); + insert into KSA_LOGISTICS_BOOKINGNOTE ( ID ) values ( 'test-bookingnote-1' ); + +-- 创建表 - 用户表 +create table KSA_SECURITY_USER ( + ID varchar(36) not null comment '用户标识' , + NAME varchar(200) not null comment '用户姓名' , + PASSWORD varchar(200) not null comment '登录密码' , + EMAIL varchar(200) not null comment '用户邮箱' , + TELEPHONE varchar(200) not null comment '用户电话' , + IS_LOCKED int(1) default 0 not null comment '是否锁定' , + primary key ( ID ) +); + + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-1', '麻文强', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456'); + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-2', '闫寅卓', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456' ); + +-- 创建表 - 基础数据 +create table KSA_BD_DATA ( + ID varchar(36) not null comment '标识', + CODE varchar(200) not null comment '编码', + NAME varchar(200) not null comment '名称', + ALIAS varchar(200) not null comment '别名', + NOTE varchar(2000) not null comment '备注', + EXTRA varchar(200) not null comment '附加属性', + TYPE_ID varchar(36) not null comment '类型标识', + RANK int not null comment '排序', + primary key ( ID ) +); + + -- 币种 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-RMB', 'RMB', '人民币', '', '', '1.000', '00-currency', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-USD', 'USD', '美元', '', '', '6.800', '00-currency', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-HKD', 'HKD', '港币', '', '', '0.882', '00-currency', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-JPY', 'JPY', '日元', '', '', '0.075', '00-currency', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-EUR', 'EUR', '欧元', '', '', '10.000', '00-currency', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-TWD', 'TWD', '台币', '', '', '0.200', '00-currency', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-KRW', 'KRW', '韩元', '', '', '0.005', '00-currency', 7 ); + -- 费用类型 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-001', 'ABF', '安保费', '', '', '', '10-charge', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-002', 'AFC', 'Air/Ocean Freight Charge', '', '', '', '10-charge', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-003', 'AWC', 'Air Waybill Charge(AWC)', '', '', '', '10-charge', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-004', 'BAF', 'BAF', '', '', '', '10-charge', 4 ); + -- 来往单位类型 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-bgh', 'BGH','报关行', '', '', '', '20-department', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-dls', 'DLS','代理商', '', '', '', '20-department', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-gys', 'GYS','供应商', '', '', '', '20-department', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-cd', 'CD','船代', '', '', '', '20-department', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-cyr', 'CYR','承运人', '', '', '', '20-department', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-chedui', 'CHEDUI','车队', '', '', '', '20-department', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-gwdl', 'GWDL','国外代理', '', '', '', '20-department', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-hkgs', 'HKGS','航空公司', '', '', '', '20-department', 8 ); + +-- 创建表 - 合作伙伴数据 +create table KSA_BD_PARTNER ( + ID varchar(36) not null comment '标识' , + CODE varchar(200) not null comment '编码' , + NAME varchar(2000) not null comment '名称' , + ALIAS varchar(2000) not null comment '别名 - 主要显示在提单信息中' , + ADDRESS varchar(2000) not null comment '地址' , + PP int not null comment '付款周期 ( 天 )' , + NOTE varchar(2000) not null comment '备注' , + IMPORTANT int(1) default 0 not null comment '是否为重要伙伴' , + RANK int not null comment '排序' , + SALER_ID varchar(36) not null comment '销售担当标识', + primary key ( ID ), + unique ( CODE ) +); + + insert into KSA_BD_PARTNER ( ID, CODE, NAME, ALIAS, ADDRESS, PP, NOTE, IMPORTANT, RANK, SALER_ID ) values ( 'test-partner-1', 'test-partner-1', '测试合作伙伴1', 'Alias1-1', '', 30, '', 1, 1, 'test-user-1'); + insert into KSA_BD_PARTNER ( ID, CODE, NAME, ALIAS, ADDRESS, PP, NOTE, IMPORTANT, RANK, SALER_ID ) values ( 'test-partner-2', 'test-partner-2', '测试合作伙伴2', 'Alias2-1', '', 30, '', 0, 1001, '' ); + + +-- ---------------------- 正式表与数据 ---------------------------- + +-- 创建表 - 结算对账单 +create table KSA_FINANCE_ACCOUNT ( + ID varchar(36) not null comment '标识', + CODE varchar(200) not null comment '结算对账单编号', + TARGET_ID varchar(36) comment '结算对象标识', + CREATOR_ID varchar(36) not null comment '创建人标识', + CREATED_DATE date comment '创建日期', + DEADLINE date comment '付款截止日期', + PAYMENT_DATE date comment '结清日期', + NOTE varchar(2000) not null comment '备注', + STATE int(5) default 0 not null comment '状态', + DIRECTION int(1) default 1 not null comment '收支方向:1表示收入,-1表示支出', + NATURE int(1) default 1 not null comment '国内/境外:1表示国内,-1表示境外', + primary key ( ID ), + + -- 外键关联合作伙伴表 + constraint FK_KSA_FINANCE_ACCOUNT_PARTNER + foreign key ( TARGET_ID ) + references KSA_BD_PARTNER ( ID ) + on delete set null + on update cascade +); + + insert into KSA_FINANCE_ACCOUNT ( ID, CODE, TARGET_ID, CREATOR_ID, CREATED_DATE, NOTE, STATE, DIRECTION ) values ( 'test-account-1', 'test-account-1', 'test-partner-1', 'test-user-1', '2012-12-11', '', 0, 1); + insert into KSA_FINANCE_ACCOUNT ( ID, CODE, TARGET_ID, CREATOR_ID, CREATED_DATE, NOTE, STATE, DIRECTION ) values ( 'test-account-2', 'test-account-2', 'test-partner-1', 'test-user-1', '2012-12-11', '', 0, -1); + + +-- 创建表 - 费用 +create table KSA_FINANCE_CHARGE ( + ID varchar(36) not null comment '标识', + TARGET_ID varchar(36) comment '费用结算对象标识', + TYPE varchar(200) not null comment '费用类型', + CURRENCY_ID varchar(36) comment '货币类型标识', + PRICE numeric(20,5) comment '单价', + QUANTITY int(10) comment '数量', + AMOUNT numeric(20,5) not null comment '金额', + CREATED_DATE date not null comment '创建日期', + NOTE varchar(2000) not null comment '备注', + CREATOR_ID varchar(36) comment '创建人标识', + ACCOUNT_ID varchar(36) comment '所属对账单标识', + BOOKINGNOTE_ID varchar(36) not null comment '所属托单标识', + DIRECTION int(1) default 1 not null comment '收支方向:1表示收入,-1表示支出', + NATURE int(1) default 1 not null comment '国内/境外:1表示国内,-1表示境外', + RANK int(3) default 0 not null comment '费用录入排序编号', + primary key ( ID ), + + -- 外键关联合作伙伴 + constraint FK_KSA_FINANCE_CHARGE_PARTNER + foreign key ( TARGET_ID ) + references KSA_BD_PARTNER ( ID ) + on delete set null + on update cascade, + -- 外键关联货币类型 + constraint FK_KSA_FINANCE_CHARGE_CURRENCY + foreign key ( CURRENCY_ID ) + references KSA_BD_DATA ( ID ) + on delete set null + on update cascade, + -- 外键关联用户表 + constraint FK_KSA_FINANCE_CHARGE_USER + foreign key ( CREATOR_ID ) + references KSA_SECURITY_USER ( ID ) + on delete set null + on update cascade, + -- 外键关联收入结算对账单表 + constraint FK_KSA_FINANCE_ACCOUNT + foreign key ( ACCOUNT_ID ) + references KSA_FINANCE_ACCOUNT ( ID ) + on delete set null + on update cascade, + -- 外键关联托单表 + constraint FK_KSA_FINANCE_BOOKINGNOTE + foreign key ( BOOKINGNOTE_ID ) + references KSA_LOGISTICS_BOOKINGNOTE ( ID ) + on delete cascade + on update cascade +); + +-- 创建表 - 发票单据:自己开出的发票(合作伙伴收到的发票 对应自己的收入费用) +create table KSA_FINANCE_INVOICE ( + ID varchar(36) not null comment '标识', + CODE varchar(200) not null comment '发票代码', + INVOICE_NUMBER varchar(200) not null comment '发票号码', + TAX_NUMBER varchar(200) not null comment '发票税号', + TYPE varchar(200) not null comment '票据类型', + TARGET_ID varchar(36) comment '票据清算对象', + CURRENCY_ID varchar(36) comment '货币类型标识', + AMOUNT numeric(20,5) not null comment '金额', + CREATED_DATE date not null comment '创建日期', + NOTE varchar(2000) not null comment '备注', + CREATOR_ID varchar(36) comment '创建人标识', + ACCOUNT_ID varchar(36) comment '所属对账单标识应用于销账', + DIRECTION int(1) default 1 not null comment '收支方向:1表示收入,-1表示支出', + NATURE int(1) default 1 not null comment '国内/境外:1表示国内,-1表示境外', + primary key ( ID ), + + -- 外键关联货币类型 + constraint FK_KSA_FINANCE_INVOICE_CURRENCY + foreign key ( CURRENCY_ID ) + references KSA_BD_DATA ( ID ) + on delete set null + on update cascade, + -- 外键关联收入结算对账单表(开出的发票与收入进行销账) + constraint FK_KSA_FINANCE_INVOICE_ACCOUNT + foreign key ( ACCOUNT_ID ) + references KSA_FINANCE_ACCOUNT ( ID ) + on delete set null + on update cascade, + -- 外键关联用户表 + constraint FK_KSA_FINANCE_INVOICE_USER + foreign key ( CREATOR_ID ) + references KSA_SECURITY_USER ( ID ) + on delete set null + on update cascade, + -- 外键关联合作伙伴表 + constraint FK_KSA_FINANCE_INVOICE_PARTNER + foreign key ( TARGET_ID ) + references KSA_BD_PARTNER ( ID ) + on delete set null + on update cascade +); + +-- 创建表 - 汇率表 : 按结算单 +create table KSA_FINANCE_CURRENCY_RATE_BYACCOUNT ( + ID varchar(36) not null comment '汇率标识', + CURRENCY_ID varchar(36) not null comment '关联货币的标识', + ACCOUNT_ID varchar(36) not null comment '关联结算单的标识', + RATE numeric(10,5) not null comment '汇率值', + primary key ( ID ), + -- 外键关联汇率表 + constraint FK_KSA_FINANCE_CURRENCY_RATE_BYACCOUNT + foreign key ( CURRENCY_ID ) + references KSA_BD_DATA ( ID ) + on delete cascade + on update cascade, + -- 外键关联结算单表 + constraint FK_KSA_FINANCE_CURRENCY_RATE_BYACCOUNT2 + foreign key ( ACCOUNT_ID ) + references KSA_FINANCE_ACCOUNT ( ID ) + on delete cascade + on update cascade +); diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/pom.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/pom.xml new file mode 100644 index 0000000..f59a21a --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.ksa + ksa-dao-root + 3.9.0 + + + ksa-logistics-dao + jar + + ksa-logistics-dao + 杭州凯思爱物流管理系统 - 物流数据管理 DAO 模块 + + + UTF-8 + + diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisArrivalNoteDao.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisArrivalNoteDao.java new file mode 100644 index 0000000..f945e47 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisArrivalNoteDao.java @@ -0,0 +1,35 @@ +package com.ksa.dao.logistics.mybatis; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.logistics.ArrivalNoteDao; +import com.ksa.model.logistics.ArrivalNote; + + +public class MybatisArrivalNoteDao extends AbstractMybatisDao implements ArrivalNoteDao { + + @Override + public int insertLogisticsModel( ArrivalNote bill ) throws RuntimeException { + return this.session.insert( "insert-logistics-arrivalnote", bill ); + } + + @Override + public int updateLogisticsModel( ArrivalNote bill ) throws RuntimeException { + return this.session.update( "update-logistics-arrivalnote", bill ); + } + + @Override + public int deleteLogisticsModel( ArrivalNote bill ) throws RuntimeException { + return this.session.delete( "delete-logistics-arrivalnote", bill ); + } + + @Override + public ArrivalNote selectLogisticsModelById( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-arrivalnote-byid", id ); + } + + @Override + public ArrivalNote selectLogisticsModelByBookingNoteId( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-arrivalnote-bybnid", id ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDao.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDao.java new file mode 100644 index 0000000..23c9d12 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDao.java @@ -0,0 +1,35 @@ +package com.ksa.dao.logistics.mybatis; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.logistics.BillOfLadingDao; +import com.ksa.model.logistics.BillOfLading; + + +public class MybatisBillOfLadingDao extends AbstractMybatisDao implements BillOfLadingDao { + + @Override + public int insertLogisticsModel( BillOfLading bill ) throws RuntimeException { + return this.session.insert( "insert-logistics-billoflading", bill ); + } + + @Override + public int updateLogisticsModel( BillOfLading bill ) throws RuntimeException { + return this.session.update( "update-logistics-billoflading", bill ); + } + + @Override + public int deleteLogisticsModel( BillOfLading bill ) throws RuntimeException { + return this.session.delete( "delete-logistics-billoflading", bill ); + } + + @Override + public BillOfLading selectLogisticsModelById( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-billoflading-byid", id ); + } + + @Override + public BillOfLading selectLogisticsModelByBookingNoteId( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-billoflading-bybnid", id ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteCargoDao.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteCargoDao.java new file mode 100644 index 0000000..14eab2e --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteCargoDao.java @@ -0,0 +1,43 @@ +package com.ksa.dao.logistics.mybatis; + +import java.util.List; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.logistics.BookingNoteCargoDao; +import com.ksa.model.logistics.BookingNoteCargo; + +/** + * 基于 Mybaits 的 CargoDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisBookingNoteCargoDao extends AbstractMybatisDao implements BookingNoteCargoDao { + + @Override + public int insertCargo( BookingNoteCargo cargo ) throws RuntimeException { + return this.session.insert( "insert-logistics-bookingnote-cargo", cargo ); + } + + @Override + public int updateCargo( BookingNoteCargo cargo ) throws RuntimeException { + return this.session.update( "update-logistics-bookingnote-cargo", cargo ); + } + + @Override + public int deleteCargo( BookingNoteCargo cargo ) throws RuntimeException { + return this.session.delete( "delete-logistics-bookingnote-cargo", cargo ); + } + + @Override + public BookingNoteCargo selectCargoById( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-bookingnote-cargo-byid", id ); + } + + @Override + public List selectCargoByBookingNoteId( String noteId ) throws RuntimeException { + return this.session.selectList( "select-logistics-bookingnote-cargo-bynoteid", noteId ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDao.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDao.java new file mode 100644 index 0000000..3805c47 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDao.java @@ -0,0 +1,86 @@ +package com.ksa.dao.logistics.mybatis; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.StringUtils; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.logistics.BookingNoteDao; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.logistics.BookingNoteState; + +/** + * 基于 Mybaits 的 BookingNoteDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisBookingNoteDao extends AbstractMybatisDao implements BookingNoteDao { + + @Override + public int insertBookingNote( BookingNote note ) throws RuntimeException { + return this.session.insert( "insert-logistics-bookingnote", note ); + } + + @Override + public int updateBookingNote( BookingNote note ) throws RuntimeException { + return this.session.update( "update-logistics-bookingnote", note ); + } + + @Override + public int updateBookingNoteState( BookingNote note ) throws RuntimeException { + return this.session.update( "update-logistics-bookingnote-state", note ); + } + + @Override + public int updateBookingNoteType( BookingNote note ) throws RuntimeException { + return this.session.update( "update-logistics-bookingnote-type", note ); + } + + @Override + public int updateBookingNoteChargeDate( BookingNote note ) throws RuntimeException { + return this.session.update( "update-logistics-bookingnote-chargedate", note ); + } + + @Override + public int deleteBookingNote( BookingNote note ) throws RuntimeException { + note.setState( BookingNoteState.DELETED ); // 将托单的状态设置为 '已删除' + return updateBookingNoteState( note ); + } + + @Override + public BookingNote selectBookingNoteById( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-bookingnote-byid", id ); + } + + @Override + public int selectBookingNoteCount() throws RuntimeException { + return ( (Integer) this.session.selectOne( "count-logistics-bookingnote" ) ).intValue(); + } + + @Override + public int selectBookingNoteCount( String queryString ) throws RuntimeException { + Map para = new HashMap(); + // FIXME 对应sql中参数名是 queryClauses,这个参数设置无用!只能查到全部数据! + para.put( "queryClause", new String[]{ queryString } ); + return ( (Integer) this.session.selectOne( "count-logistics-bookingnote-query", para ) ).intValue(); + } + + @Override + public BookingNote selectBookingNoteByLading( BookingNote note ) throws RuntimeException { + if( !StringUtils.hasText( note.getHawb() ) && !StringUtils.hasText( note.getMawb() ) ) { + throw new IllegalArgumentException( "通过提单号查询托单时,主副提单号不能同时为空。" ); + } + BookingNote param = new BookingNote(); + param.setId( note.getId() ); + if( StringUtils.hasText( note.getMawb() ) ) { + param.setMawb( note.getMawb() ); + } + if( StringUtils.hasText( note.getHawb() ) ) { + param.setHawb( note.getHawb() ); + } + return this.session.selectOne( "select-logistics-bookingnote-bylading", param ); + } +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisManifestDao.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisManifestDao.java new file mode 100644 index 0000000..9bb566c --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisManifestDao.java @@ -0,0 +1,35 @@ +package com.ksa.dao.logistics.mybatis; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.logistics.ManifestDao; +import com.ksa.model.logistics.Manifest; + + +public class MybatisManifestDao extends AbstractMybatisDao implements ManifestDao { + + @Override + public int insertLogisticsModel( Manifest bill ) throws RuntimeException { + return this.session.insert( "insert-logistics-manifest", bill ); + } + + @Override + public int updateLogisticsModel( Manifest bill ) throws RuntimeException { + return this.session.update( "update-logistics-manifest", bill ); + } + + @Override + public int deleteLogisticsModel( Manifest bill ) throws RuntimeException { + return this.session.delete( "delete-logistics-manifest", bill ); + } + + @Override + public Manifest selectLogisticsModelById( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-manifest-byid", id ); + } + + @Override + public Manifest selectLogisticsModelByBookingNoteId( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-manifest-bybnid", id ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDao.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDao.java new file mode 100644 index 0000000..3ab9f2b --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDao.java @@ -0,0 +1,35 @@ +package com.ksa.dao.logistics.mybatis; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.logistics.WarehouseBookingDao; +import com.ksa.model.logistics.WarehouseBooking; + + +public class MybatisWarehouseBookingDao extends AbstractMybatisDao implements WarehouseBookingDao { + + @Override + public int insertLogisticsModel( WarehouseBooking bill ) throws RuntimeException { + return this.session.insert( "insert-logistics-warehousebooking", bill ); + } + + @Override + public int updateLogisticsModel( WarehouseBooking bill ) throws RuntimeException { + return this.session.update( "update-logistics-warehousebooking", bill ); + } + + @Override + public int deleteLogisticsModel( WarehouseBooking bill ) throws RuntimeException { + return this.session.delete( "delete-logistics-warehousebooking", bill ); + } + + @Override + public WarehouseBooking selectLogisticsModelById( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-warehousebooking-byid", id ); + } + + @Override + public WarehouseBooking selectLogisticsModelByBookingNoteId( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-warehousebooking-bybnid", id ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDao.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDao.java new file mode 100644 index 0000000..eac88ef --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDao.java @@ -0,0 +1,35 @@ +package com.ksa.dao.logistics.mybatis; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.logistics.WarehouseNotingDao; +import com.ksa.model.logistics.WarehouseNoting; + + +public class MybatisWarehouseNotingDao extends AbstractMybatisDao implements WarehouseNotingDao { + + @Override + public int insertLogisticsModel( WarehouseNoting bill ) throws RuntimeException { + return this.session.insert( "insert-logistics-warehousenoting", bill ); + } + + @Override + public int updateLogisticsModel( WarehouseNoting bill ) throws RuntimeException { + return this.session.update( "update-logistics-warehousenoting", bill ); + } + + @Override + public int deleteLogisticsModel( WarehouseNoting bill ) throws RuntimeException { + return this.session.delete( "delete-logistics-warehousenoting", bill ); + } + + @Override + public WarehouseNoting selectLogisticsModelById( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-warehousenoting-byid", id ); + } + + @Override + public WarehouseNoting selectLogisticsModelByBookingNoteId( String id ) throws RuntimeException { + return this.session.selectOne( "select-logistics-warehousenoting-bybnid", id ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-arrivalnote.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-arrivalnote.xml new file mode 100644 index 0000000..8764431 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-arrivalnote.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_LOGISTICS_ARRIVALNOTE + ( ID, ARRIVAL_DATE, CODE, SHIPPER, CONSIGNEE, + VESSEL, VOYAGE, MAWB, HAWB, CONTAINER, SEAL, ETA, + CY, LOADING_PORT, DISCHARGE_PORT, DELIVER_PLACE, + CARGO_MARK, CARGO_WEIGHT, CARGO_VOLUMN, CARGO_DESCRIPTION, + CARGO, CARGO_PKG, CARGO_COUNT, FREIGHT, CHARGE, RATE, BOOKINGNOTE_ID ) + VALUES ( #{id,jdbcType=VARCHAR}, #{date,jdbcType=VARCHAR}, #{code,jdbcType=VARCHAR}, #{shipper,jdbcType=VARCHAR}, #{consignee,jdbcType=VARCHAR}, + #{vessel,jdbcType=VARCHAR}, #{voyage,jdbcType=VARCHAR}, #{mawb,jdbcType=VARCHAR}, #{hawb,jdbcType=VARCHAR}, #{container,jdbcType=VARCHAR}, #{seal,jdbcType=VARCHAR}, #{eta,jdbcType=VARCHAR}, + #{cy,jdbcType=VARCHAR},#{loadingPort,jdbcType=VARCHAR},#{dischargePort,jdbcType=VARCHAR}, #{deliverPlace,jdbcType=VARCHAR}, + #{cargoMark,jdbcType=VARCHAR}, #{cargoWeight,jdbcType=VARCHAR},#{cargoVolumn,jdbcType=VARCHAR},#{cargoDescription,jdbcType=VARCHAR}, + #{cargo,jdbcType=VARCHAR}, #{pkg,jdbcType=VARCHAR}, #{count,jdbcType=VARCHAR}, #{freight,jdbcType=VARCHAR}, #{charge,jdbcType=VARCHAR}, #{rate,jdbcType=VARCHAR}, #{bookingNote.id} ) + + + + UPDATE KSA_LOGISTICS_ARRIVALNOTE SET + ARRIVAL_DATE = #{date,jdbcType=VARCHAR}, + CODE = #{code,jdbcType=VARCHAR}, + SHIPPER = #{shipper,jdbcType=VARCHAR}, + CONSIGNEE = #{consignee,jdbcType=VARCHAR}, + VESSEL = #{vessel,jdbcType=VARCHAR}, + VOYAGE = #{voyage,jdbcType=VARCHAR}, + MAWB = #{mawb,jdbcType=VARCHAR}, + HAWB = #{hawb,jdbcType=VARCHAR}, + CONTAINER = #{container,jdbcType=VARCHAR}, + SEAL = #{seal,jdbcType=VARCHAR}, + ETA = #{eta,jdbcType=VARCHAR}, + CY = #{cy,jdbcType=VARCHAR}, + LOADING_PORT = #{loadingPort,jdbcType=VARCHAR}, + DISCHARGE_PORT = #{dischargePort,jdbcType=VARCHAR}, + DELIVER_PLACE = #{deliverPlace,jdbcType=VARCHAR}, + CARGO_MARK = #{cargoMark,jdbcType=VARCHAR}, + CARGO_WEIGHT = #{cargoWeight,jdbcType=VARCHAR}, + CARGO_VOLUMN = #{cargoVolumn,jdbcType=VARCHAR}, + CARGO_DESCRIPTION = #{cargoDescription,jdbcType=VARCHAR}, + + CARGO = #{cargo,jdbcType=VARCHAR}, + CARGO_PKG = #{pkg,jdbcType=VARCHAR}, + CARGO_COUNT = #{count,jdbcType=VARCHAR}, + + CHARGE = #{charge,jdbcType=VARCHAR}, + FREIGHT = #{freight,jdbcType=VARCHAR}, + RATE = #{rate,jdbcType=VARCHAR} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + DELETE FROM KSA_LOGISTICS_ARRIVALNOTE WHERE ID = #{id,jdbcType=VARCHAR} + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-billoflading.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-billoflading.xml new file mode 100644 index 0000000..14e4fba --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-billoflading.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_LOGISTICS_BILLOFLADING + ( ID, TARGET, PUBLISH_DATE, SHIPPER, CONSIGNEE, NOTIFY, CODE, + DELIVER_TYPE, CUSTOMER_CODE, SELF_CODE, CREATOR, BILL_TYPE, + NOTE, AGENT, VESSEL_VOYAGE, LOADING_PORT, DISCHARGE_PORT, DESTINATION_PORT, + CARGO_MARK, CARGO_QUANTITY, CARGO_NAME,CARGO_WEIGHT, CARGO_VOLUMN, CARGO_DESCRIPTION, + CARGO_QUANTITY_DESCRIPTION, PAY_MODE, BOOKINGNOTE_ID ) + VALUES ( #{id,jdbcType=VARCHAR}, #{to,jdbcType=VARCHAR}, #{publishDate,jdbcType=VARCHAR}, #{shipper,jdbcType=VARCHAR}, #{consignee,jdbcType=VARCHAR},#{notify,jdbcType=VARCHAR},#{code,jdbcType=VARCHAR}, + #{deliverType,jdbcType=VARCHAR}, #{customerCode,jdbcType=VARCHAR}, #{selfCode,jdbcType=VARCHAR}, #{creator,jdbcType=VARCHAR}, #{billType,jdbcType=VARCHAR}, + #{note,jdbcType=VARCHAR},#{agent,jdbcType=VARCHAR},#{vesselVoyage,jdbcType=VARCHAR},#{loadingPort,jdbcType=VARCHAR},#{dischargePort,jdbcType=VARCHAR}, #{destinationPort,jdbcType=VARCHAR}, + #{cargoMark,jdbcType=VARCHAR}, #{cargoQuantity,jdbcType=VARCHAR}, #{cargoName,jdbcType=VARCHAR}, #{cargoWeight,jdbcType=VARCHAR},#{cargoVolumn,jdbcType=VARCHAR},#{cargoDescription,jdbcType=VARCHAR}, + #{cargoQuantityDescription,jdbcType=VARCHAR}, #{payMode,jdbcType=VARCHAR}, #{bookingNote.id} ) + + + + UPDATE KSA_LOGISTICS_BILLOFLADING SET + TARGET = #{to,jdbcType=VARCHAR}, + PUBLISH_DATE = #{publishDate,jdbcType=VARCHAR}, + SHIPPER = #{shipper,jdbcType=VARCHAR}, + CONSIGNEE = #{consignee,jdbcType=VARCHAR}, + NOTIFY = #{notify,jdbcType=VARCHAR}, + CODE = #{code,jdbcType=VARCHAR}, + DELIVER_TYPE = #{deliverType,jdbcType=VARCHAR}, + CUSTOMER_CODE = #{customerCode,jdbcType=VARCHAR}, + SELF_CODE = #{selfCode,jdbcType=VARCHAR}, + CREATOR = #{creator,jdbcType=VARCHAR}, + BILL_TYPE = #{billType,jdbcType=VARCHAR}, + NOTE = #{note,jdbcType=VARCHAR}, + AGENT = #{agent,jdbcType=VARCHAR}, + VESSEL_VOYAGE = #{vesselVoyage,jdbcType=VARCHAR}, + LOADING_PORT = #{loadingPort,jdbcType=VARCHAR}, + DISCHARGE_PORT = #{dischargePort,jdbcType=VARCHAR}, + DESTINATION_PORT= #{destinationPort,jdbcType=VARCHAR}, + CARGO_MARK = #{cargoMark,jdbcType=VARCHAR}, + CARGO_QUANTITY = #{cargoQuantity,jdbcType=VARCHAR}, + CARGO_NAME = #{cargoName,jdbcType=VARCHAR}, + CARGO_WEIGHT = #{cargoWeight,jdbcType=VARCHAR}, + CARGO_VOLUMN = #{cargoVolumn,jdbcType=VARCHAR}, + CARGO_DESCRIPTION = #{cargoDescription,jdbcType=VARCHAR}, + PAY_MODE = #{payMode,jdbcType=VARCHAR}, + CARGO_QUANTITY_DESCRIPTION = #{cargoQuantityDescription,jdbcType=VARCHAR} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + DELETE FROM KSA_LOGISTICS_BILLOFLADING WHERE ID = #{id,jdbcType=VARCHAR} + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote-cargo.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote-cargo.xml new file mode 100644 index 0000000..2acbbc1 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote-cargo.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + INSERT INTO KSA_LOGISTICS_BOOKINGNOTE_CARGO + ( ID, NAME, CATEGORY, TYPE, AMOUNT, BOOKINGNOTE_ID ) + VALUES ( #{id,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{category,jdbcType=VARCHAR}, + #{type,jdbcType=VARCHAR}, #{amount,jdbcType=NUMERIC}, #{bookingNote.id,jdbcType=VARCHAR} ) + + + + UPDATE KSA_LOGISTICS_BOOKINGNOTE_CARGO SET + NAME = #{name,jdbcType=VARCHAR}, + CATEGORY = #{category,jdbcType=VARCHAR}, + TYPE = #{type,jdbcType=VARCHAR}, + AMOUNT = #{amount,jdbcType=NUMERIC} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + DELETE FROM KSA_LOGISTICS_BOOKINGNOTE_CARGO WHERE ID = #{id,jdbcType=VARCHAR} + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote.xml new file mode 100644 index 0000000..342c463 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-bookingnote.xml @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_LOGISTICS_BOOKINGNOTE + ( ID, CODE, TYPE, TYPE_SUB, SERIAL_NUMBER, CUSTOMER_ID, INVOICE_NUMBER, CREATED_DATE, CHARGE_DATE, + SALER_ID, CREATOR_ID, CARRIER_ID, SHIPPING_AGENT_ID, AGENT_ID, + CARGO_NAME, CARGO_NAME_ENG, CARGO_PRICE, HS_CODE, KEY_CONTENT, + CARGO_NOTE, CARGO_CONTAINER, CARGO_DESCRIPTION, SHIPPING_MARK, VOLUMN, WEIGHT, QUANTITY, UNIT,QUANTITY_DESCRIPTION, + TITLE, MAWB, HAWB, SHIPPER_ID, CONSIGNEE_ID, NOTIFY_ID, + DEPARTURE, DEPARTURE_PORT, DEPARTURE_DATE, DESTINATION, DESTINATION_PORT, DESTINATION_DATE, LOADING_PORT, DISCHARGE_PORT, DELIVER_DATE, + ROUTE, ROUTE_NAME, ROUTE_CODE, + CUSTOMS_BROKER_ID, + CUSTOMS_CODE, CUSTOMS_DATE, RETURN_CODE, RETURN_DATE, RETURN_DATE2, + TAX_CODE, TAX_DATE1, EXPRESS_CODE, TAX_DATE2, + VEHICLE_TEAM_ID, VEHICLE_TYPE, VEHICLE_NUMBER, STATE ) + VALUES ( #{id,jdbcType=VARCHAR}, #{code,jdbcType=VARCHAR}, #{type,jdbcType=VARCHAR}, #{subType,jdbcType=VARCHAR}, #{serialNumber}, #{customer.id,jdbcType=VARCHAR},#{invoiceNumber,jdbcType=VARCHAR},#{createdDate,jdbcType=DATE},#{chargeDate,jdbcType=DATE}, + #{saler.id,jdbcType=VARCHAR}, #{creator.id,jdbcType=VARCHAR}, #{carrier.id,jdbcType=VARCHAR}, #{shippingAgent.id,jdbcType=VARCHAR}, #{agent.id,jdbcType=VARCHAR}, + #{cargoName,jdbcType=VARCHAR},#{cargoNameEng,jdbcType=VARCHAR},#{cargoPrice,jdbcType=VARCHAR},#{hsCode,jdbcType=VARCHAR},#{keyContent,jdbcType=VARCHAR}, + #{cargoNote,jdbcType=VARCHAR},#{cargoContainer,jdbcType=VARCHAR},#{cargoDescription,jdbcType=VARCHAR},#{shippingMark,jdbcType=VARCHAR}, #{volumn,jdbcType=NUMERIC}, #{weight,jdbcType=NUMERIC}, #{quantity,jdbcType=NUMERIC},#{unit,jdbcType=VARCHAR},#{quantityDescription,jdbcType=VARCHAR}, + #{title,jdbcType=VARCHAR}, #{mawb,jdbcType=VARCHAR}, #{hawb,jdbcType=VARCHAR}, #{shipper.id,jdbcType=VARCHAR},#{consignee.id,jdbcType=VARCHAR},#{notify.id,jdbcType=VARCHAR}, + #{departure,jdbcType=VARCHAR},#{departurePort,jdbcType=VARCHAR}, #{departureDate,jdbcType=DATE},#{destination,jdbcType=VARCHAR},#{destinationPort,jdbcType=VARCHAR},#{destinationDate,jdbcType=DATE},#{loadingPort,jdbcType=VARCHAR}, #{dischargePort,jdbcType=VARCHAR}, #{deliverDate,jdbcType=DATE}, + #{route,jdbcType=VARCHAR},#{routeName,jdbcType=VARCHAR},#{routeCode,jdbcType=VARCHAR}, + #{customsBroker.id,jdbcType=VARCHAR}, + #{customsCode,jdbcType=VARCHAR},#{customsDate,jdbcType=DATE},#{returnCode,jdbcType=VARCHAR},#{returnDate,jdbcType=DATE},#{returnDate2,jdbcType=DATE}, + #{taxCode,jdbcType=VARCHAR},#{taxDate1,jdbcType=DATE},#{expressCode,jdbcType=VARCHAR},#{taxDate2,jdbcType=DATE}, + #{vehicleTeam.id,jdbcType=VARCHAR},#{vehicleType,jdbcType=VARCHAR},#{vehicleNumber,jdbcType=VARCHAR},#{state,jdbcType=NUMERIC} ) + + + + + UPDATE KSA_LOGISTICS_BOOKINGNOTE SET + TYPE_SUB = #{subType,jdbcType=VARCHAR}, + CUSTOMER_ID = #{customer.id,jdbcType=VARCHAR}, + INVOICE_NUMBER = #{invoiceNumber,jdbcType=VARCHAR}, + CREATED_DATE = #{createdDate,jdbcType=DATE}, + + + SALER_ID = #{saler.id,jdbcType=VARCHAR}, + CARRIER_ID = #{carrier.id,jdbcType=VARCHAR}, + SHIPPING_AGENT_ID = #{shippingAgent.id,jdbcType=VARCHAR}, + AGENT_ID = #{agent.id,jdbcType=VARCHAR}, + + CARGO_NAME = #{cargoName,jdbcType=VARCHAR}, + CARGO_NAME_ENG = #{cargoNameEng,jdbcType=VARCHAR}, + CARGO_PRICE = #{cargoPrice,jdbcType=VARCHAR}, + KEY_CONTENT = #{keyContent,jdbcType=VARCHAR}, + HS_CODE = #{hsCode,jdbcType=VARCHAR}, + CARGO_NOTE = #{cargoNote,jdbcType=VARCHAR}, + CARGO_DESCRIPTION = #{cargoDescription,jdbcType=VARCHAR}, + CARGO_CONTAINER = #{cargoContainer,jdbcType=VARCHAR}, + SHIPPING_MARK = #{shippingMark,jdbcType=VARCHAR}, + VOLUMN = #{volumn,jdbcType=NUMERIC}, + WEIGHT = #{weight,jdbcType=NUMERIC}, + QUANTITY = #{quantity,jdbcType=NUMERIC}, + UNIT = #{unit,jdbcType=VARCHAR}, + QUANTITY_DESCRIPTION= #{quantityDescription,jdbcType=VARCHAR}, + + TITLE = #{title,jdbcType=VARCHAR}, + MAWB = #{mawb,jdbcType=VARCHAR}, + HAWB = #{hawb,jdbcType=VARCHAR}, + SHIPPER_ID = #{shipper.id,jdbcType=VARCHAR}, + CONSIGNEE_ID = #{consignee.id,jdbcType=VARCHAR}, + NOTIFY_ID = #{notify.id,jdbcType=VARCHAR}, + + DEPARTURE = #{departure,jdbcType=VARCHAR}, + DEPARTURE_PORT = #{departurePort,jdbcType=VARCHAR}, + DEPARTURE_DATE = #{departureDate,jdbcType=DATE}, + DESTINATION = #{destination,jdbcType=VARCHAR}, + DESTINATION_PORT = #{destinationPort,jdbcType=VARCHAR}, + DESTINATION_DATE = #{destinationDate,jdbcType=DATE}, + LOADING_PORT = #{loadingPort,jdbcType=VARCHAR}, + DISCHARGE_PORT = #{dischargePort,jdbcType=VARCHAR}, + DELIVER_DATE = #{deliverDate,jdbcType=DATE}, + + ROUTE = #{route,jdbcType=VARCHAR}, + ROUTE_NAME = #{routeName,jdbcType=VARCHAR}, + ROUTE_CODE = #{routeCode,jdbcType=VARCHAR}, + + CUSTOMS_BROKER_ID = #{customsBroker.id,jdbcType=VARCHAR}, + CUSTOMS_CODE = #{customsCode,jdbcType=VARCHAR}, + CUSTOMS_DATE = #{customsDate,jdbcType=DATE}, + RETURN_CODE = #{returnCode,jdbcType=VARCHAR}, + RETURN_DATE = #{returnDate,jdbcType=DATE}, + RETURN_DATE2 = #{returnDate2,jdbcType=DATE}, + TAX_CODE = #{taxCode,jdbcType=VARCHAR}, + TAX_DATE1 = #{taxDate1,jdbcType=DATE}, + EXPRESS_CODE = #{expressCode,jdbcType=VARCHAR}, + TAX_DATE2 = #{taxDate2,jdbcType=DATE}, + + VEHICLE_TEAM_ID = #{vehicleTeam.id,jdbcType=VARCHAR}, + VEHICLE_TYPE = #{vehicleType,jdbcType=VARCHAR}, + VEHICLE_NUMBER = #{vehicleNumber,jdbcType=VARCHAR} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + UPDATE KSA_LOGISTICS_BOOKINGNOTE SET + STATE = #{state,jdbcType=NUMERIC} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + UPDATE KSA_LOGISTICS_BOOKINGNOTE SET + TYPE = #{type,jdbcType=VARCHAR}, + CODE = #{code,jdbcType=VARCHAR} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + + UPDATE KSA_LOGISTICS_BOOKINGNOTE SET + CHARGE_DATE = #{chargeDate,jdbcType=DATE} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + SELECT bn.*, + c1.NAME AS CUSTOMER_NAME, c1.CODE AS CUSTOMER_CODE, + u1.NAME AS CREATOR_NAME, + u2.NAME AS SALER_NAME, + p1.NAME AS CARRIER_NAME, + p2.NAME AS SHIPPING_AGENT_NAME, + p3.NAME AS SHIPPER_NAME, p3.ALIAS AS SHIPPER_ALIAS, + p4.NAME AS CONSIGNEE_NAME, p4.ALIAS AS CONSIGNEE_ALIAS, + p5.NAME AS NOTIFY_NAME, p5.ALIAS AS NOTIFY_ALIAS, + s1.NAME AS AGENT_NAME, + s2.NAME AS CUSTOMS_BROKER_NAME, + s3.NAME AS VEHICLE_TEAM_NAME + FROM KSA_LOGISTICS_BOOKINGNOTE bn + LEFT JOIN KSA_BD_PARTNER c1 ON bn.CUSTOMER_ID = c1.ID + LEFT JOIN KSA_SECURITY_USER u1 ON bn.CREATOR_ID = u1.ID + LEFT JOIN KSA_SECURITY_USER u2 ON bn.SALER_ID = u2.ID + LEFT JOIN KSA_BD_PARTNER p1 ON bn.CARRIER_ID = p1.ID + LEFT JOIN KSA_BD_PARTNER p2 ON bn.SHIPPING_AGENT_ID = p2.ID + LEFT JOIN KSA_BD_PARTNER p3 ON bn.SHIPPER_ID = p3.ID + LEFT JOIN KSA_BD_PARTNER p4 ON bn.CONSIGNEE_ID = p4.ID + LEFT JOIN KSA_BD_PARTNER p5 ON bn.NOTIFY_ID = p5.ID + LEFT JOIN KSA_BD_PARTNER s1 ON bn.AGENT_ID = s1.ID + LEFT JOIN KSA_BD_PARTNER s2 ON bn.CUSTOMS_BROKER_ID = s2.ID + LEFT JOIN KSA_BD_PARTNER s3 ON bn.VEHICLE_TEAM_ID = s3.ID + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-manifest.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-manifest.xml new file mode 100644 index 0000000..d6ca81c --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-manifest.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_LOGISTICS_MANIFEST + ( ID, SALER, CODE, FLIGHT_DATE, LOADING_PORT, DESTINATION_PORT, AGENT, + HAWB, CARGO_NAME, CARGO_WEIGHT, FINAL_DESTINATION, + SHIPPER, CONSIGNEE, RE, + TOTAL_HAWB, TOTAL_PACKAGES, + BOOKINGNOTE_ID, + SALER_TEL, SALER_FAX, SALER_EMAIL ) + VALUES ( #{id,jdbcType=VARCHAR}, #{saler,jdbcType=VARCHAR}, #{code,jdbcType=VARCHAR}, #{flightDate,jdbcType=VARCHAR},#{loadingPort,jdbcType=VARCHAR}, #{destinationPort,jdbcType=VARCHAR}, #{agent,jdbcType=VARCHAR}, + #{hawb,jdbcType=VARCHAR}, #{cargoName,jdbcType=VARCHAR}, #{cargoWeight,jdbcType=VARCHAR}, #{finalDestination,jdbcType=VARCHAR}, + #{shipper,jdbcType=VARCHAR}, #{consignee,jdbcType=VARCHAR},#{re,jdbcType=VARCHAR}, + #{totalHawb,jdbcType=VARCHAR},#{totalPackages,jdbcType=VARCHAR}, + #{bookingNote.id}, + #{salerTel,jdbcType=VARCHAR}, #{salerFax,jdbcType=VARCHAR}, #{salerEmail,jdbcType=VARCHAR} ) + + + + UPDATE KSA_LOGISTICS_MANIFEST SET + SALER = #{saler,jdbcType=VARCHAR}, + CODE = #{code,jdbcType=VARCHAR}, + FLIGHT_DATE = #{flightDate,jdbcType=VARCHAR}, + LOADING_PORT = #{loadingPort,jdbcType=VARCHAR}, + DESTINATION_PORT= #{destinationPort,jdbcType=VARCHAR}, + AGENT = #{agent,jdbcType=VARCHAR}, + + HAWB = #{hawb,jdbcType=VARCHAR}, + CARGO_NAME = #{cargoName,jdbcType=VARCHAR}, + CARGO_WEIGHT = #{cargoWeight,jdbcType=VARCHAR}, + FINAL_DESTINATION = #{finalDestination,jdbcType=VARCHAR}, + SHIPPER = #{shipper,jdbcType=VARCHAR}, + CONSIGNEE = #{consignee,jdbcType=VARCHAR}, + RE = #{re,jdbcType=VARCHAR}, + + TOTAL_HAWB = #{totalHawb,jdbcType=VARCHAR}, + TOTAL_PACKAGES = #{totalPackages,jdbcType=VARCHAR}, + + SALER_TEL = #{salerTel,jdbcType=VARCHAR}, + SALER_FAX = #{salerFax,jdbcType=VARCHAR}, + SALER_EMAIL = #{salerEmail,jdbcType=VARCHAR} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + DELETE FROM KSA_LOGISTICS_MANIFEST WHERE ID = #{id,jdbcType=VARCHAR} + + + + + diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-booking.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-booking.xml new file mode 100644 index 0000000..aca6698 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-booking.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_LOGISTICS_WAREHOUSEBOOKING + ( ID, SALER, SHIPPER, CONSIGNEE, NOTIFY, + CODE, CREATED_DATE, DEPARTURE_PORT, DESTINATION_PORT, + SWITCH_SHIP, GROUPING, TRANSPORT_MODE, PAYMENT_MODE, FREIGHT_CHARGE, + CARGO_CONTAINER, SHIPPING_MARK, CARGO_NAME, + CARGO_WEIGHT, CARGO_VOLUMN, CARGO_QUANTITY, + TOTAL_WEIGHT, TOTAL_VOLUMN, TOTAL_QUANTITY, + NOTE, + BOOKINGNOTE_ID, SALER_TEL, SALER_FAX, SALER_EMAIL ) + VALUES ( #{id,jdbcType=VARCHAR}, #{saler,jdbcType=VARCHAR}, #{shipper,jdbcType=VARCHAR}, #{consignee,jdbcType=VARCHAR},#{notify,jdbcType=VARCHAR}, + #{code,jdbcType=VARCHAR}, #{createdDate,jdbcType=VARCHAR},#{departurePort,jdbcType=VARCHAR}, #{destinationPort,jdbcType=VARCHAR}, + #{switchShip,jdbcType=VARCHAR}, #{grouping,jdbcType=VARCHAR},#{transportMode,jdbcType=VARCHAR}, #{paymentMode,jdbcType=VARCHAR}, #{freightCharge,jdbcType=VARCHAR}, + #{cargoContainer,jdbcType=VARCHAR}, #{shippingMark,jdbcType=VARCHAR}, #{cargoName,jdbcType=VARCHAR}, + #{cargoWeight,jdbcType=VARCHAR},#{cargoVolumn,jdbcType=VARCHAR},#{cargoQuantity,jdbcType=VARCHAR}, + #{totalWeight,jdbcType=VARCHAR},#{totalVolumn,jdbcType=VARCHAR},#{totalQuantity,jdbcType=VARCHAR}, + #{note,jdbcType=VARCHAR}, + #{bookingNote.id}, #{salerTel,jdbcType=VARCHAR}, #{salerFax,jdbcType=VARCHAR}, #{salerEmail,jdbcType=VARCHAR} ) + + + + UPDATE KSA_LOGISTICS_WAREHOUSEBOOKING SET + SALER = #{saler,jdbcType=VARCHAR}, + SHIPPER = #{shipper,jdbcType=VARCHAR}, + CONSIGNEE = #{consignee,jdbcType=VARCHAR}, + NOTIFY = #{notify,jdbcType=VARCHAR}, + + CODE = #{code,jdbcType=VARCHAR}, + CREATED_DATE = #{createdDate,jdbcType=VARCHAR}, + DEPARTURE_PORT = #{departurePort,jdbcType=VARCHAR}, + DESTINATION_PORT= #{destinationPort,jdbcType=VARCHAR}, + + SWITCH_SHIP = #{switchShip,jdbcType=VARCHAR}, + GROUPING = #{grouping,jdbcType=VARCHAR}, + TRANSPORT_MODE = #{transportMode,jdbcType=VARCHAR}, + PAYMENT_MODE = #{paymentMode,jdbcType=VARCHAR}, + FREIGHT_CHARGE = #{freightCharge,jdbcType=VARCHAR}, + + CARGO_CONTAINER= #{cargoContainer,jdbcType=VARCHAR}, + SHIPPING_MARK = #{shippingMark,jdbcType=VARCHAR}, + CARGO_NAME = #{cargoName,jdbcType=VARCHAR}, + CARGO_WEIGHT = #{cargoWeight,jdbcType=VARCHAR}, + CARGO_VOLUMN = #{cargoVolumn,jdbcType=VARCHAR}, + CARGO_QUANTITY = #{cargoQuantity,jdbcType=VARCHAR}, + TOTAL_WEIGHT = #{totalWeight,jdbcType=VARCHAR}, + TOTAL_VOLUMN = #{totalVolumn,jdbcType=VARCHAR}, + TOTAL_QUANTITY = #{totalQuantity,jdbcType=VARCHAR}, + NOTE = #{note,jdbcType=VARCHAR}, + SALER_TEL = #{salerTel,jdbcType=VARCHAR}, + SALER_FAX = #{salerFax,jdbcType=VARCHAR}, + SALER_EMAIL = #{salerEmail,jdbcType=VARCHAR} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + DELETE FROM KSA_LOGISTICS_WAREHOUSEBOOKING WHERE ID = #{id,jdbcType=VARCHAR} + + + + + diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-noting.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-noting.xml new file mode 100644 index 0000000..d6a4a06 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/mybatis/mapper/logistics-warehouse-noting.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_LOGISTICS_WAREHOUSENOTING + ( ID, SALER, TARGET, CODE, CREATED_DATE, + CARGO_NAME, CARGO_WEIGHT, CARGO_VOLUMN, CARGO_QUANTITY, + CUSTOMER,LOADING_PORT, DISCHARGE_PORT, VESSEL_VOYAGE, + DESTINATION, DEPARTURE_DATE, MAWB, ENTRY_DATE, INFORM_DATE, + ADDRESS, CONTACT, TELEPHONE, FAX, + BOOKINGNOTE_ID, SALER_TEL, SALER_FAX, SALER_EMAIL ) + VALUES ( #{id,jdbcType=VARCHAR}, #{saler,jdbcType=VARCHAR}, #{to,jdbcType=VARCHAR}, #{code,jdbcType=VARCHAR}, #{createdDate,jdbcType=VARCHAR}, + #{cargoName,jdbcType=VARCHAR}, #{cargoWeight,jdbcType=VARCHAR},#{cargoVolumn,jdbcType=VARCHAR},#{cargoQuantity,jdbcType=VARCHAR}, + #{customer,jdbcType=VARCHAR}, #{loadingPort,jdbcType=VARCHAR},#{dischargePort,jdbcType=VARCHAR},#{vesselVoyage,jdbcType=VARCHAR}, + #{destination,jdbcType=VARCHAR}, #{departureDate,jdbcType=VARCHAR},#{mawb,jdbcType=VARCHAR},#{entryDate,jdbcType=VARCHAR}, #{informDate,jdbcType=VARCHAR}, + #{address,jdbcType=VARCHAR}, #{contact,jdbcType=VARCHAR},#{telephone,jdbcType=VARCHAR},#{fax,jdbcType=VARCHAR}, + #{bookingNote.id}, #{salerTel,jdbcType=VARCHAR}, #{salerFax,jdbcType=VARCHAR}, #{salerEmail,jdbcType=VARCHAR} ) + + + + UPDATE KSA_LOGISTICS_WAREHOUSENOTING SET + SALER = #{saler,jdbcType=VARCHAR}, + TARGET = #{to,jdbcType=VARCHAR}, + CODE = #{code,jdbcType=VARCHAR}, + CREATED_DATE = #{createdDate,jdbcType=VARCHAR}, + CARGO_NAME = #{cargoName,jdbcType=VARCHAR}, + CARGO_WEIGHT = #{cargoWeight,jdbcType=VARCHAR}, + CARGO_VOLUMN = #{cargoVolumn,jdbcType=VARCHAR}, + CARGO_QUANTITY = #{cargoQuantity,jdbcType=VARCHAR}, + CUSTOMER = #{customer,jdbcType=VARCHAR}, + LOADING_PORT = #{loadingPort,jdbcType=VARCHAR}, + DISCHARGE_PORT = #{dischargePort,jdbcType=VARCHAR}, + VESSEL_VOYAGE = #{vesselVoyage,jdbcType=VARCHAR}, + DESTINATION = #{destination,jdbcType=VARCHAR}, + DEPARTURE_DATE = #{departureDate,jdbcType=VARCHAR}, + MAWB = #{mawb,jdbcType=VARCHAR}, + ENTRY_DATE = #{entryDate,jdbcType=VARCHAR}, + INFORM_DATE = #{informDate,jdbcType=VARCHAR}, + ADDRESS = #{address,jdbcType=VARCHAR}, + CONTACT = #{contact,jdbcType=VARCHAR}, + TELEPHONE = #{telephone,jdbcType=VARCHAR}, + FAX = #{fax,jdbcType=VARCHAR}, + SALER_TEL = #{salerTel,jdbcType=VARCHAR}, + SALER_FAX = #{salerFax,jdbcType=VARCHAR}, + SALER_EMAIL = #{salerEmail,jdbcType=VARCHAR} + WHERE ID = #{id,jdbcType=VARCHAR} + + + + DELETE FROM KSA_LOGISTICS_WAREHOUSENOTING WHERE ID = #{id,jdbcType=VARCHAR} + + + + + diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/spring/dao/logistics-dao-context.xml b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/spring/dao/logistics-dao-context.xml new file mode 100644 index 0000000..3c41490 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/main/resources/spring/dao/logistics-dao-context.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDaoTest.java new file mode 100644 index 0000000..3bc59cb --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBillOfLadingDaoTest.java @@ -0,0 +1,130 @@ +package com.ksa.dao.logistics.mybatis; + +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.logistics.BillOfLadingDao; +import com.ksa.model.logistics.BillOfLading; + +public class MybatisBillOfLadingDaoTest extends MybatisDaoTest { + + @Test + public void testCrudBillOfLading() throws RuntimeException { + BillOfLadingDao dao = CONTEXT.getBean( "billOfLadingDao", BillOfLadingDao.class ); + + BillOfLading bill = new BillOfLading(); + String id = UUID.randomUUID().toString(); + bill.setId( id ); + bill.setAgent( "agent" ); + bill.setBillType( "type" ); + bill.getBookingNote().setId( "test-bookingnote-1" ); + bill.setCargoDescription( "cargoDescription" ); + bill.setCargoMark( "cargoMark" ); + bill.setCargoName( "cargoName" ); + bill.setCargoQuantity( "cargoQuantity" ); + bill.setCargoVolumn( "cargoVolumn" ); + bill.setCargoWeight( "cargoWeight" ); + bill.setCode( "code" ); + bill.setConsignee( "consignee" ); + bill.setCreator( "creator" ); + bill.setCustomerCode( "customerCode" ); + bill.setDeliverType( "deliverType" ); + bill.setDestinationPort( "destinationPort" ); + bill.setDischargePort( "dischargePort" ); + bill.setLoadingPort( "loadingPort" ); + bill.setNote( "note" ); + bill.setNotify( "notify" ); + bill.setPublishDate( "publishDate" ); + bill.setSelfCode( "selfCode" ); + bill.setShipper( "shipper" ); + bill.setVesselVoyage( "vesselVoyage" ); + bill.setTo("to"); + + // 测试插入是否成功 + dao.insertLogisticsModel( bill ); + BillOfLading temp = ( BillOfLading ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "agent", temp.getAgent() ); + Assert.assertEquals( "type", temp.getBillType() ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + Assert.assertEquals( "cargoDescription", temp.getCargoDescription() ); + Assert.assertEquals( "cargoMark", temp.getCargoMark() ); + Assert.assertEquals( "cargoName", temp.getCargoName() ); + Assert.assertEquals( "cargoQuantity", temp.getCargoQuantity() ); + Assert.assertEquals( "cargoVolumn", temp.getCargoVolumn() ); + Assert.assertEquals( "cargoWeight", temp.getCargoWeight() ); + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "consignee", temp.getConsignee() ); + Assert.assertEquals( "creator", temp.getCreator() ); + Assert.assertEquals( "customerCode", temp.getCustomerCode() ); + Assert.assertEquals( "deliverType", temp.getDeliverType() ); + Assert.assertEquals( "destinationPort", temp.getDestinationPort() ); + Assert.assertEquals( "dischargePort", temp.getDischargePort() ); + Assert.assertEquals( "loadingPort", temp.getLoadingPort() ); + Assert.assertEquals( "note", temp.getNote() ); + Assert.assertEquals( "notify", temp.getNotify() ); + Assert.assertEquals( "publishDate", temp.getPublishDate() ); + Assert.assertEquals( "selfCode", temp.getSelfCode() ); + Assert.assertEquals( "shipper", temp.getShipper() ); + Assert.assertEquals( "vesselVoyage", temp.getVesselVoyage() ); + Assert.assertEquals( "to", temp.getTo() ); + + + // 开始更新 + bill.setAgent( "agent1" ); + bill.setBillType( "type1" ); + bill.getBookingNote().setId( "test-bookingnote-2" ); + bill.setCargoDescription( "cargoDescription1" ); + bill.setCargoMark( "cargoMark1" ); + bill.setCargoName( "cargoName1" ); + bill.setCargoQuantity( "cargoQuantity1" ); + bill.setCargoVolumn( "cargoVolumn1" ); + bill.setCargoWeight( "cargoWeight1" ); + bill.setCode( "code1" ); + bill.setConsignee( "consignee1" ); + bill.setCreator( "creator1" ); + bill.setCustomerCode( "customerCode1" ); + bill.setDeliverType( "deliverType1" ); + bill.setDestinationPort( "destinationPort1" ); + bill.setDischargePort( "dischargePort1" ); + bill.setLoadingPort( "loadingPort1" ); + bill.setNote( "note1" ); + bill.setNotify( "notify1" ); + bill.setPublishDate( "publishDate1" ); + bill.setSelfCode( "selfCode1" ); + bill.setShipper( "shipper1" ); + bill.setVesselVoyage( "vesselVoyage1" ); + bill.setTo("to1"); + + // 测试更新是否成功 + dao.updateLogisticsModel( bill ); + temp = ( BillOfLading ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "agent1", temp.getAgent() ); + Assert.assertEquals( "type1", temp.getBillType() ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + Assert.assertEquals( "cargoDescription1", temp.getCargoDescription() ); + Assert.assertEquals( "cargoMark1", temp.getCargoMark() ); + Assert.assertEquals( "cargoName1", temp.getCargoName() ); + Assert.assertEquals( "cargoQuantity1", temp.getCargoQuantity() ); + Assert.assertEquals( "cargoVolumn1", temp.getCargoVolumn() ); + Assert.assertEquals( "cargoWeight1", temp.getCargoWeight() ); + Assert.assertEquals( "code1", temp.getCode() ); + Assert.assertEquals( "consignee1", temp.getConsignee() ); + Assert.assertEquals( "creator1", temp.getCreator() ); + Assert.assertEquals( "customerCode1", temp.getCustomerCode() ); + Assert.assertEquals( "deliverType1", temp.getDeliverType() ); + Assert.assertEquals( "destinationPort1", temp.getDestinationPort() ); + Assert.assertEquals( "dischargePort1", temp.getDischargePort() ); + Assert.assertEquals( "loadingPort1", temp.getLoadingPort() ); + Assert.assertEquals( "note1", temp.getNote() ); + Assert.assertEquals( "notify1", temp.getNotify() ); + Assert.assertEquals( "publishDate1", temp.getPublishDate() ); + Assert.assertEquals( "selfCode1", temp.getSelfCode() ); + Assert.assertEquals( "shipper1", temp.getShipper() ); + Assert.assertEquals( "vesselVoyage1", temp.getVesselVoyage() ); + Assert.assertEquals( "to1", temp.getTo() ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDaoTest.java new file mode 100644 index 0000000..a6f376a --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisBookingNoteDaoTest.java @@ -0,0 +1,354 @@ +package com.ksa.dao.logistics.mybatis; + +import java.text.DateFormat; +import java.util.Date; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.logistics.BookingNoteCargoDao; +import com.ksa.dao.logistics.BookingNoteDao; +import com.ksa.model.bd.Partner; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.logistics.BookingNoteCargo; +import com.ksa.model.logistics.BookingNoteState; +import com.ksa.model.security.User; + +public class MybatisBookingNoteDaoTest extends MybatisDaoTest { + + private static User TEST_USER1 = new User(); + private static User TEST_USER2 = new User(); + private static Partner TEST_PARTNER1 = new Partner(); + private static Partner TEST_PARTNER2 = new Partner(); + static { + TEST_USER1.setId( "test-user-1" ); + TEST_USER2.setId( "test-user-2" ); + TEST_PARTNER1.setId( "test-partner-1" ); + TEST_PARTNER2.setId( "test-partner-2" ); + } + + @Test + public void testCrudBookingNote() throws RuntimeException { + BookingNoteDao noteDao = CONTEXT.getBean( "bookingNoteDao", BookingNoteDao.class ); + BookingNoteCargoDao cargoDao = CONTEXT.getBean( "bookingNoteCargoDao", BookingNoteCargoDao.class ); + + BookingNote note = new BookingNote(); + String id = UUID.randomUUID().toString(); + Date now = new Date(); + DateFormat df = DateFormat.getDateInstance(); + note.setId( id ); + note.setCode( "code" ); + note.setType( "se" ); + note.setSubType( "fcl" ); + note.setSerialNumber( 1 ); + note.setCustomer( TEST_PARTNER1 ); + note.setInvoiceNumber( "invoiceNumber" ); + note.setCreatedDate( now ); + + note.setSaler( TEST_USER1 ); + note.setCreator( TEST_USER1 ); + note.setCarrier( TEST_PARTNER1 ); + note.setShippingAgent( TEST_PARTNER1 ); + note.getAgent().setId( "aaa" ); + + note.setCargoName( "cargoName" ); + note.setCargoNote( "cargoNote" ); + note.setCargoDescription( "cargoDescription" ); + note.setCargoContainer( "cargoContainer" ); + note.setShippingMark( "shippingMark" ); + note.setVolumn( 1.0f ); + note.setWeight( 2.0f ); + note.setQuantity( 3 ); + note.setUnit( "unit" ); + note.setQuantityDescription( "quantityDescription" ); + + note.setTitle( "title" ); + note.setMawb( "mawb" ); + note.setHawb( "hawb" ); + note.setShipper( TEST_PARTNER1 ); + note.setConsignee( TEST_PARTNER1 ); + note.setNotify( TEST_PARTNER1 ); + + note.setDeparture( "departure" ); + note.setDepartureDate( now ); + note.setDeparturePort( "departurePort" ); + note.setDestination("destination" ); + note.setDestinationDate( now ); + note.setDestinationPort( "destinationPort" ); + note.setLoadingPort( "loadingPort" ); + note.setDischargePort( "dischargePort" ); + note.setDeliverDate( now ); + note.setRoute( "route" ); + note.setRouteCode( "routeCode" ); + note.setRouteName( "routeName" ); + + note.setCustomsBroker( TEST_PARTNER1 ); + note.setCustomsCode( "customsCode" ); + note.setCustomsDate( now ); + note.setReturnCode( "returnCode" ); + note.setReturnDate( now ); + note.setReturnDate2( now ); + note.setTaxCode( "taxCode" ); + note.setTaxDate1( now ); + note.setTaxDate2( now ); + note.setExpressCode( "expressCode" ); + + note.setVehicleType( "vehicleType" ); + note.setVehicleNumber( "vehicleNumber" ); + note.setVehicleTeam( TEST_PARTNER1 ); + + note.setState( 4 ); + + // 测试插入是否成功 + noteDao.insertBookingNote( note ); + BookingNote temp = noteDao.selectBookingNoteById( id ); + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "se", temp.getType() ); + Assert.assertEquals( "fcl", temp.getSubType() ); + Assert.assertEquals( 1, temp.getSerialNumber() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getCustomer().getId() ); + Assert.assertEquals( "invoiceNumber", temp.getInvoiceNumber() ); + Assert.assertEquals( df.format( now ), df.format( temp.getCreatedDate() ) ); + + Assert.assertEquals( TEST_USER1.getId(), temp.getSaler().getId() ); + Assert.assertEquals( TEST_USER1.getId(), temp.getCreator().getId() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getCarrier().getId() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getShippingAgent().getId() ); + Assert.assertEquals( "aaa", temp.getAgent().getId() ); + + Assert.assertEquals( "cargoName", temp.getCargoName() ); + Assert.assertEquals( "cargoNote", temp.getCargoNote() ); + Assert.assertEquals( "cargoDescription", temp.getCargoDescription() ); + Assert.assertEquals( "cargoContainer", temp.getCargoContainer() ); + Assert.assertEquals( "shippingMark", temp.getShippingMark() ); + Assert.assertEquals( 1.0f, temp.getVolumn(), 0.001 ); + Assert.assertEquals( 2.0f, temp.getWeight(), 0.001 ); + Assert.assertEquals( 3, temp.getQuantity().intValue() ); + Assert.assertEquals( "unit", temp.getUnit() ); + Assert.assertEquals( "quantityDescription", temp.getQuantityDescription() ); + + Assert.assertEquals( "title", temp.getTitle() ); + Assert.assertEquals( "mawb", temp.getMawb() ); + Assert.assertEquals( "hawb", temp.getHawb() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getShipper().getId() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getConsignee().getId() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getNotify().getId() ); + + Assert.assertEquals( "departure", temp.getDeparture() ); + Assert.assertEquals( "departurePort", temp.getDeparturePort() ); + Assert.assertEquals( df.format( now ), df.format( temp.getDepartureDate() ) ); + Assert.assertEquals( "destination", temp.getDestination() ); + Assert.assertEquals( "destinationPort", temp.getDestinationPort() ); + Assert.assertEquals( df.format( now ), df.format( temp.getDestinationDate() ) ); + Assert.assertEquals( "loadingPort", temp.getLoadingPort() ); + Assert.assertEquals( "dischargePort", temp.getDischargePort() ); + Assert.assertEquals( df.format( now ), df.format( temp.getDeliverDate() ) ); + + Assert.assertEquals( "route", temp.getRoute() ); + Assert.assertEquals( "routeName", temp.getRouteName() ); + Assert.assertEquals( "routeCode", temp.getRouteCode() ); + + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getCustomsBroker().getId() ); + Assert.assertEquals( "customsCode", temp.getCustomsCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getCustomsDate() ) ); + Assert.assertEquals( "returnCode", temp.getReturnCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getReturnDate() ) ); + Assert.assertEquals( df.format( now ), df.format( temp.getReturnDate2() ) ); + Assert.assertEquals( "taxCode", temp.getTaxCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getTaxDate2() ) ); + Assert.assertEquals( "expressCode", temp.getExpressCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getTaxDate1() ) ); + + Assert.assertEquals( "vehicleType", temp.getVehicleType() ); + Assert.assertEquals( "vehicleNumber", temp.getVehicleNumber() ); + Assert.assertEquals( TEST_PARTNER1.getId(), temp.getVehicleTeam().getId() ); + + Assert.assertEquals( 4, temp.getState() ); + + + // 开始更新 + now = new Date(); + note.setCode( "code1" ); + note.setType( "ae" ); + note.setSubType( "lcl" ); + note.setSerialNumber( 2 ); + note.setCustomer( TEST_PARTNER2 ); + note.setInvoiceNumber( "invoiceNumber1" ); + note.setCreatedDate( now ); + + note.setSaler( TEST_USER2 ); + note.setCreator( TEST_USER2 ); + note.setCarrier( TEST_PARTNER2 ); + note.setShippingAgent( TEST_PARTNER2 ); + note.getAgent().setId( "bbb" ); + + note.setCargoName( "cargoName1" ); + note.setCargoNote( "cargoNote1" ); + note.setCargoDescription( "cargoDescription1" ); + note.setCargoContainer( "cargoContainer1" ); + note.setShippingMark( "shippingMark1" ); + note.setVolumn( 2.0f ); + note.setWeight( 3.0f ); + note.setQuantity( 4 ); + note.setUnit( "unit1" ); + note.setQuantityDescription( "quantityDescription1" ); + + note.setTitle( "title1" ); + note.setMawb( "mawb1" ); + note.setHawb( "hawb1" ); + note.setShipper( TEST_PARTNER2 ); + note.setConsignee( TEST_PARTNER2 ); + note.setNotify( TEST_PARTNER2 ); + + note.setDeparture( "departure1" ); + note.setDepartureDate( now ); + note.setDeparturePort( "departurePort1" ); + note.setDestination("destination1" ); + note.setDestinationDate( now ); + note.setDestinationPort( "destinationPort1" ); + note.setLoadingPort( "loadingPort1" ); + note.setDischargePort( "dischargePort1" ); + note.setDeliverDate( now ); + note.setRoute( "route1" ); + note.setRouteCode( "routeCode1" ); + note.setRouteName( "routeName1" ); + + note.setCustomsBroker( TEST_PARTNER2 ); + note.setCustomsCode( "customsCode1" ); + note.setCustomsDate( now ); + note.setReturnCode( "returnCode1" ); + note.setReturnDate( now ); + note.setReturnDate2( now ); + note.setTaxCode( "taxCode1" ); + note.setTaxDate1( now ); + note.setTaxDate2( now ); + note.setExpressCode( "expressCode1" ); + + note.setVehicleType( "vehicleType1" ); + note.setVehicleNumber( "vehicleNumber1" ); + note.setVehicleTeam( TEST_PARTNER2 ); + + note.setState( 5 ); + + // 测试更新是否成功 + noteDao.updateBookingNote( note ); + temp = noteDao.selectBookingNoteById( id ); + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "se", temp.getType() ); + Assert.assertEquals( "lcl", temp.getSubType() ); + Assert.assertEquals( 1, temp.getSerialNumber() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getCustomer().getId() ); + Assert.assertEquals( "invoiceNumber1", temp.getInvoiceNumber() ); + Assert.assertEquals( df.format( now ), df.format( temp.getCreatedDate() ) ); + + Assert.assertEquals( TEST_USER2.getId(), temp.getSaler().getId() ); + Assert.assertEquals( TEST_USER1.getId(), temp.getCreator().getId() ); // 创建人不能变更 + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getCarrier().getId() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getShippingAgent().getId() ); + Assert.assertEquals( "bbb", temp.getAgent().getId() ); + + Assert.assertEquals( "cargoName1", temp.getCargoName() ); + Assert.assertEquals( "cargoNote1", temp.getCargoNote() ); + Assert.assertEquals( "cargoDescription1", temp.getCargoDescription() ); + Assert.assertEquals( "cargoContainer1", temp.getCargoContainer() ); + Assert.assertEquals( "shippingMark1", temp.getShippingMark() ); + Assert.assertEquals( 2.0f, temp.getVolumn(), 0.001 ); + Assert.assertEquals( 3.0f, temp.getWeight(), 0.001 ); + Assert.assertEquals( 4, temp.getQuantity().intValue() ); + Assert.assertEquals( "unit1", temp.getUnit() ); + Assert.assertEquals( "quantityDescription1", temp.getQuantityDescription() ); + + Assert.assertEquals( "title1", temp.getTitle() ); + Assert.assertEquals( "mawb1", temp.getMawb() ); + Assert.assertEquals( "hawb1", temp.getHawb() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getShipper().getId() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getConsignee().getId() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getNotify().getId() ); + + Assert.assertEquals( "departure1", temp.getDeparture() ); + Assert.assertEquals( "departurePort1", temp.getDeparturePort() ); + Assert.assertEquals( df.format( now ), df.format( temp.getDepartureDate() ) ); + Assert.assertEquals( "destination1", temp.getDestination() ); + Assert.assertEquals( "destinationPort1", temp.getDestinationPort() ); + Assert.assertEquals( df.format( now ), df.format( temp.getDestinationDate() ) ); + Assert.assertEquals( "loadingPort1", temp.getLoadingPort() ); + Assert.assertEquals( "dischargePort1", temp.getDischargePort() ); + Assert.assertEquals( df.format( now ), df.format( temp.getDeliverDate() ) ); + + Assert.assertEquals( "route1", temp.getRoute() ); + Assert.assertEquals( "routeName1", temp.getRouteName() ); + Assert.assertEquals( "routeCode1", temp.getRouteCode() ); + + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getCustomsBroker().getId() ); + Assert.assertEquals( "customsCode1", temp.getCustomsCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getCustomsDate() ) ); + Assert.assertEquals( "returnCode1", temp.getReturnCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getReturnDate() ) ); + Assert.assertEquals( df.format( now ), df.format( temp.getReturnDate2() ) ); + Assert.assertEquals( "taxCode1", temp.getTaxCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getTaxDate2() ) ); + Assert.assertEquals( "expressCode1", temp.getExpressCode() ); + Assert.assertEquals( df.format( now ), df.format( temp.getTaxDate1() ) ); + + Assert.assertEquals( "vehicleType1", temp.getVehicleType() ); + Assert.assertEquals( "vehicleNumber1", temp.getVehicleNumber() ); + Assert.assertEquals( TEST_PARTNER2.getId(), temp.getVehicleTeam().getId() ); + + // 更新时不修改托单状态,在 updateBookingNoteState 中单独进行更改 + Assert.assertEquals( 4, temp.getState() ); + + + // 测试删除 + noteDao.deleteBookingNote( note ); + BookingNote temp3 = noteDao.selectBookingNoteById( id ); + Assert.assertTrue( temp3.getState() == BookingNoteState.DELETED ); + + +// 测试 Cargo 详细信息的增删改查 + BookingNoteCargo cargo = new BookingNoteCargo(); + String cargoId = UUID.randomUUID().toString(); + cargo.setId( cargoId ); + cargo.setAmount( 1 ); + cargo.setCategory( "category" ); + cargo.setType( "type" ); + cargo.setName( "name" ); + cargo.setBookingNote( note ); + // 增 + cargoDao.insertCargo( cargo ); + BookingNoteCargo cargoTemp =cargoDao.selectCargoById( cargoId ); + Assert.assertEquals( "category", cargoTemp.getCategory() ); + Assert.assertEquals( "type", cargoTemp.getType() ); + Assert.assertEquals( "name", cargoTemp.getName() ); + Assert.assertEquals( 1, cargoTemp.getAmount() ); + Assert.assertEquals( id, cargoTemp.getBookingNote().getId() ); + // 更新 + cargo.setAmount( 2 ); + cargo.setCategory( "category1" ); + cargo.setType( "type1" ); + cargo.setName( "name1" ); + cargoDao.updateCargo( cargo ); + cargoTemp =cargoDao.selectCargoById( cargoId ); + Assert.assertEquals( "category1", cargoTemp.getCategory() ); + Assert.assertEquals( "type1", cargoTemp.getType() ); + Assert.assertEquals( "name1", cargoTemp.getName() ); + Assert.assertEquals( 2, cargoTemp.getAmount() ); + Assert.assertEquals( id, cargoTemp.getBookingNote().getId() ); + + temp = noteDao.selectBookingNoteById( id ); + Assert.assertEquals( 1, temp.getCargos().size() ); + cargoTemp = temp.getCargos().get( 0 ); + Assert.assertEquals( "category1", cargoTemp.getCategory() ); + Assert.assertEquals( "type1", cargoTemp.getType() ); + Assert.assertEquals( "name1", cargoTemp.getName() ); + Assert.assertEquals( 2, cargoTemp.getAmount() ); + + // 删除 + cargoDao.deleteCargo( cargo ); + cargoTemp =cargoDao.selectCargoById( cargoId ); + Assert.assertNull( cargoTemp ); + + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisManifestDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisManifestDaoTest.java new file mode 100644 index 0000000..ac5994b --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisManifestDaoTest.java @@ -0,0 +1,109 @@ +package com.ksa.dao.logistics.mybatis; + +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.logistics.ManifestDao; +import com.ksa.model.logistics.Manifest; + +public class MybatisManifestDaoTest extends MybatisDaoTest { + + @Test + public void testCrudManifest() throws RuntimeException { + ManifestDao dao = CONTEXT.getBean( "manifestDao", ManifestDao.class ); + + Manifest bill = new Manifest(); + String id = UUID.randomUUID().toString(); + bill.setId( id ); + bill.getBookingNote().setId( "test-bookingnote-1" ); + + bill.setSaler( "saler" ); + bill.setCode( "code" ); + bill.setFlightDate( "createdDate" ); + bill.setLoadingPort( "departurePort" ); + bill.setDestinationPort( "destinationPort" ); + bill.setAgent( "agent" ); + + bill.setHawb( "hawb" ); + bill.setCargoName( "cargoName" ); + bill.setCargoWeight( "cargoWeight" ); + bill.setFinalDestination( "finalDestination" ); + bill.setShipper( "shipper" ); + bill.setConsignee( "consignee" ); + bill.setRe( "re" ); + + bill.setTotalHawb( "totalVolumn" ); + bill.setTotalPackages( "totalWeight" ); + + // 测试插入是否成功 + dao.insertLogisticsModel( bill ); + Manifest temp = ( Manifest ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + + Assert.assertEquals( "saler", temp.getSaler() ); + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "createdDate", temp.getFlightDate() ); + Assert.assertEquals( "departurePort", temp.getLoadingPort() ); + Assert.assertEquals( "destinationPort", temp.getDestinationPort() ); + Assert.assertEquals( "agent", temp.getAgent() ); + + Assert.assertEquals( "shipper", temp.getShipper() ); + Assert.assertEquals( "consignee", temp.getConsignee() ); + Assert.assertEquals( "re", temp.getRe() ); + + Assert.assertEquals( "hawb", temp.getHawb() ); + Assert.assertEquals( "cargoName", temp.getCargoName() ); + Assert.assertEquals( "cargoWeight", temp.getCargoWeight() ); + Assert.assertEquals( "finalDestination", temp.getFinalDestination() ); + Assert.assertEquals( "totalVolumn", temp.getTotalHawb() ); + Assert.assertEquals( "totalWeight", temp.getTotalPackages() ); + + // 开始更新 + bill.getBookingNote().setId( "test-bookingnote-2" ); + + bill.setSaler( "saler1" ); + bill.setCode( "code1" ); + bill.setFlightDate( "createdDate1" ); + bill.setLoadingPort( "departurePort1" ); + bill.setDestinationPort( "destinationPort1" ); + bill.setAgent( "agent1" ); + + bill.setHawb( "hawb1" ); + bill.setCargoName( "cargoName1" ); + bill.setCargoWeight( "cargoWeight1" ); + bill.setFinalDestination( "finalDestination1" ); + bill.setShipper( "shipper1" ); + bill.setConsignee( "consignee1" ); + bill.setRe( "re1" ); + + bill.setTotalHawb( "totalVolumn1" ); + bill.setTotalPackages( "totalWeight1" ); + + // 测试插入是否成功 + dao.updateLogisticsModel( bill ); + temp = ( Manifest ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + + Assert.assertEquals( "saler1", temp.getSaler() ); + Assert.assertEquals( "code1", temp.getCode() ); + Assert.assertEquals( "createdDate1", temp.getFlightDate() ); + Assert.assertEquals( "departurePort1", temp.getLoadingPort() ); + Assert.assertEquals( "destinationPort1", temp.getDestinationPort() ); + Assert.assertEquals( "agent1", temp.getAgent() ); + + Assert.assertEquals( "shipper1", temp.getShipper() ); + Assert.assertEquals( "consignee1", temp.getConsignee() ); + Assert.assertEquals( "re1", temp.getRe() ); + + Assert.assertEquals( "hawb1", temp.getHawb() ); + Assert.assertEquals( "cargoName1", temp.getCargoName() ); + Assert.assertEquals( "cargoWeight1", temp.getCargoWeight() ); + Assert.assertEquals( "finalDestination1", temp.getFinalDestination() ); + Assert.assertEquals( "totalVolumn1", temp.getTotalHawb() ); + Assert.assertEquals( "totalWeight1", temp.getTotalPackages() ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDaoTest.java new file mode 100644 index 0000000..5af991d --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseBookingDaoTest.java @@ -0,0 +1,146 @@ +package com.ksa.dao.logistics.mybatis; + +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.logistics.WarehouseBookingDao; +import com.ksa.model.logistics.WarehouseBooking; + +public class MybatisWarehouseBookingDaoTest extends MybatisDaoTest { + + @Test + public void testCrudWarehouseBooking() throws RuntimeException { + WarehouseBookingDao dao = CONTEXT.getBean( "warehouseBookingDao", WarehouseBookingDao.class ); + + WarehouseBooking bill = new WarehouseBooking(); + String id = UUID.randomUUID().toString(); + bill.setId( id ); + bill.getBookingNote().setId( "test-bookingnote-1" ); + + bill.setSaler( "saler" ); + bill.setShipper( "shipper" ); + bill.setConsignee( "consignee" ); + bill.setNotify( "notify" ); + + bill.setCode( "code" ); + bill.setCreatedDate( "createdDate" ); + bill.setSwitchShip( "switchShip" ); + bill.setGrouping( "grouping" ); + bill.setDeparturePort( "departurePort" ); + bill.setDestinationPort( "destinationPort" ); + bill.setFreightCharge( "freightCharge" ); + bill.setTransportMode( "transportMode" ); + bill.setPaymentMode( "paymentMode" ); + + bill.setCargoContainer( "cargoContainer" ); + bill.setShippingMark( "shippingMark" ); + bill.setCargoName( "cargoName" ); + bill.setCargoQuantity( "cargoQuantity" ); + bill.setCargoVolumn( "cargoVolumn" ); + bill.setCargoWeight( "cargoWeight" ); + bill.setTotalQuantity( "totalQuantity" ); + bill.setTotalVolumn( "totalVolumn" ); + bill.setTotalWeight( "totalWeight" ); + + bill.setNote( "note" ); + + // 测试插入是否成功 + dao.insertLogisticsModel( bill ); + WarehouseBooking temp = ( WarehouseBooking ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + + + Assert.assertEquals( "saler", temp.getSaler() ); + Assert.assertEquals( "shipper", temp.getShipper() ); + Assert.assertEquals( "consignee", temp.getConsignee() ); + Assert.assertEquals( "notify", temp.getNotify() ); + + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "createdDate", temp.getCreatedDate() ); + Assert.assertEquals( "switchShip", temp.getSwitchShip() ); + Assert.assertEquals( "grouping", temp.getGrouping() ); + Assert.assertEquals( "freightCharge", temp.getFreightCharge() ); + Assert.assertEquals( "transportMode", temp.getTransportMode() ); + Assert.assertEquals( "paymentMode", temp.getPaymentMode() ); + Assert.assertEquals( "departurePort", temp.getDeparturePort() ); + Assert.assertEquals( "destinationPort", temp.getDestinationPort() ); + + Assert.assertEquals( "cargoContainer", temp.getCargoContainer() ); + Assert.assertEquals( "shippingMark", temp.getShippingMark() ); + Assert.assertEquals( "cargoName", temp.getCargoName() ); + Assert.assertEquals( "cargoQuantity", temp.getCargoQuantity() ); + Assert.assertEquals( "cargoVolumn", temp.getCargoVolumn() ); + Assert.assertEquals( "cargoWeight", temp.getCargoWeight() ); + Assert.assertEquals( "totalQuantity", temp.getTotalQuantity() ); + Assert.assertEquals( "totalVolumn", temp.getTotalVolumn() ); + Assert.assertEquals( "totalWeight", temp.getTotalWeight() ); + + Assert.assertEquals( "note", temp.getNote() ); + + // 开始更新 + bill.getBookingNote().setId( "test-bookingnote-2" ); + bill.setSaler( "saler1" ); + bill.setShipper( "shipper1" ); + bill.setConsignee( "consignee1" ); + bill.setNotify( "notify1" ); + + bill.setCode( "code1" ); + bill.setCreatedDate( "createdDate1" ); + bill.setSwitchShip( "switchShip1" ); + bill.setGrouping( "grouping1" ); + bill.setDeparturePort( "departurePort1" ); + bill.setDestinationPort( "destinationPort1" ); + bill.setFreightCharge( "freightCharge1" ); + bill.setTransportMode( "transportMode1" ); + bill.setPaymentMode( "paymentMode1" ); + + bill.setCargoContainer( "cargoContainer1" ); + bill.setShippingMark( "shippingMark1" ); + bill.setCargoName( "cargoName1" ); + bill.setCargoQuantity( "cargoQuantity1" ); + bill.setCargoVolumn( "cargoVolumn1" ); + bill.setCargoWeight( "cargoWeight1" ); + bill.setTotalQuantity( "totalQuantity1" ); + bill.setTotalVolumn( "totalVolumn1" ); + bill.setTotalWeight( "totalWeight1" ); + + bill.setNote( "note1" ); + + // 测试插入是否成功 + dao.updateLogisticsModel( bill ); + temp = ( WarehouseBooking ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + + + Assert.assertEquals( "saler1", temp.getSaler() ); + Assert.assertEquals( "shipper1", temp.getShipper() ); + Assert.assertEquals( "consignee1", temp.getConsignee() ); + Assert.assertEquals( "notify1", temp.getNotify() ); + + Assert.assertEquals( "code1", temp.getCode() ); + Assert.assertEquals( "createdDate1", temp.getCreatedDate() ); + Assert.assertEquals( "switchShip1", temp.getSwitchShip() ); + Assert.assertEquals( "grouping1", temp.getGrouping() ); + Assert.assertEquals( "freightCharge1", temp.getFreightCharge() ); + Assert.assertEquals( "transportMode1", temp.getTransportMode() ); + Assert.assertEquals( "paymentMode1", temp.getPaymentMode() ); + Assert.assertEquals( "departurePort1", temp.getDeparturePort() ); + Assert.assertEquals( "destinationPort1", temp.getDestinationPort() ); + + Assert.assertEquals( "cargoContainer1", temp.getCargoContainer() ); + Assert.assertEquals( "shippingMark1", temp.getShippingMark() ); + Assert.assertEquals( "cargoName1", temp.getCargoName() ); + Assert.assertEquals( "cargoQuantity1", temp.getCargoQuantity() ); + Assert.assertEquals( "cargoVolumn1", temp.getCargoVolumn() ); + Assert.assertEquals( "cargoWeight1", temp.getCargoWeight() ); + Assert.assertEquals( "totalQuantity1", temp.getTotalQuantity() ); + Assert.assertEquals( "totalVolumn1", temp.getTotalVolumn() ); + Assert.assertEquals( "totalWeight1", temp.getTotalWeight() ); + + Assert.assertEquals( "note1", temp.getNote() ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDaoTest.java new file mode 100644 index 0000000..244e4b9 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/java/com/ksa/dao/logistics/mybatis/MybatisWarehouseNotingDaoTest.java @@ -0,0 +1,117 @@ +package com.ksa.dao.logistics.mybatis; + +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.logistics.WarehouseNotingDao; +import com.ksa.model.logistics.WarehouseNoting; + +public class MybatisWarehouseNotingDaoTest extends MybatisDaoTest { + + @Test + public void testCrudBillOfLading() throws RuntimeException { + WarehouseNotingDao dao = CONTEXT.getBean( "warehouseNotingDao", WarehouseNotingDao.class ); + + WarehouseNoting bill = new WarehouseNoting(); + String id = UUID.randomUUID().toString(); + bill.setId( id ); + bill.getBookingNote().setId( "test-bookingnote-1" ); + bill.setCargoName( "cargoName" ); + bill.setCargoQuantity( "cargoQuantity" ); + bill.setCargoVolumn( "cargoVolumn" ); + bill.setCargoWeight( "cargoWeight" ); + bill.setCode( "code" ); + bill.setContact( "contact" ); + bill.setCreatedDate( "createdDate" ); + bill.setCustomer( "customer" ); + bill.setDepartureDate( "departureDate" ); + bill.setDestination( "destination" ); + bill.setDischargePort( "dischargePort" ); + bill.setEntryDate( "entryDate" ); + bill.setFax( "fax" ); + bill.setInformDate( "informDate" ); + bill.setMawb( "mawb" ); + bill.setLoadingPort( "loadingPort" ); + bill.setSaler( "saler" ); + bill.setTelephone( "telephone" ); + bill.setVesselVoyage( "vesselVoyage" ); + bill.setTo("to"); + + // 测试插入是否成功 + dao.insertLogisticsModel( bill ); + WarehouseNoting temp = ( WarehouseNoting ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + Assert.assertEquals( "cargoName", temp.getCargoName() ); + Assert.assertEquals( "cargoQuantity", temp.getCargoQuantity() ); + Assert.assertEquals( "cargoVolumn", temp.getCargoVolumn() ); + Assert.assertEquals( "cargoWeight", temp.getCargoWeight() ); + Assert.assertEquals( "code", temp.getCode() ); + Assert.assertEquals( "contact", temp.getContact() ); + Assert.assertEquals( "createdDate", temp.getCreatedDate() ); + Assert.assertEquals( "customer", temp.getCustomer() ); + Assert.assertEquals( "departureDate", temp.getDepartureDate() ); + Assert.assertEquals( "destination", temp.getDestination() ); + Assert.assertEquals( "dischargePort", temp.getDischargePort() ); + Assert.assertEquals( "loadingPort", temp.getLoadingPort() ); + Assert.assertEquals( "entryDate", temp.getEntryDate() ); + Assert.assertEquals( "fax", temp.getFax() ); + Assert.assertEquals( "informDate", temp.getInformDate() ); + Assert.assertEquals( "mawb", temp.getMawb() ); + Assert.assertEquals( "saler", temp.getSaler() ); + Assert.assertEquals( "telephone", temp.getTelephone() ); + Assert.assertEquals( "vesselVoyage", temp.getVesselVoyage() ); + Assert.assertEquals( "to", temp.getTo() ); + + // 开始更新 + bill.getBookingNote().setId( "test-bookingnote-2" ); + bill.setCargoName( "cargoName1" ); + bill.setCargoQuantity( "cargoQuantity1" ); + bill.setCargoVolumn( "cargoVolumn1" ); + bill.setCargoWeight( "cargoWeight1" ); + bill.setCode( "code1" ); + bill.setContact( "contact1" ); + bill.setCreatedDate( "createdDate1" ); + bill.setCustomer( "customer1" ); + bill.setDepartureDate( "departureDate1" ); + bill.setDestination( "destination1" ); + bill.setDischargePort( "dischargePort1" ); + bill.setEntryDate( "entryDate1" ); + bill.setFax( "fax1" ); + bill.setInformDate( "informDate1" ); + bill.setMawb( "mawb1" ); + bill.setLoadingPort( "loadingPort1" ); + bill.setSaler( "saler1" ); + bill.setTelephone( "telephone1" ); + bill.setVesselVoyage( "vesselVoyage1" ); + bill.setTo("to1"); + + // 测试插入是否成功 + dao.updateLogisticsModel( bill ); + temp = ( WarehouseNoting ) ( dao.selectLogisticsModelById(id) ); + Assert.assertEquals( "test-bookingnote-1", temp.getBookingNote().getId() ); + Assert.assertEquals( "cargoName1", temp.getCargoName() ); + Assert.assertEquals( "cargoQuantity1", temp.getCargoQuantity() ); + Assert.assertEquals( "cargoVolumn1", temp.getCargoVolumn() ); + Assert.assertEquals( "cargoWeight1", temp.getCargoWeight() ); + Assert.assertEquals( "code1", temp.getCode() ); + Assert.assertEquals( "contact1", temp.getContact() ); + Assert.assertEquals( "createdDate1", temp.getCreatedDate() ); + Assert.assertEquals( "customer1", temp.getCustomer() ); + Assert.assertEquals( "departureDate1", temp.getDepartureDate() ); + Assert.assertEquals( "destination1", temp.getDestination() ); + Assert.assertEquals( "dischargePort1", temp.getDischargePort() ); + Assert.assertEquals( "loadingPort1", temp.getLoadingPort() ); + Assert.assertEquals( "entryDate1", temp.getEntryDate() ); + Assert.assertEquals( "fax1", temp.getFax() ); + Assert.assertEquals( "informDate1", temp.getInformDate() ); + Assert.assertEquals( "mawb1", temp.getMawb() ); + Assert.assertEquals( "saler1", temp.getSaler() ); + Assert.assertEquals( "telephone1", temp.getTelephone() ); + Assert.assertEquals( "vesselVoyage1", temp.getVesselVoyage() ); + Assert.assertEquals( "to1", temp.getTo() ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/resources/init.sql b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/resources/init.sql new file mode 100644 index 0000000..d9c2d9f --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-logistics-dao/src/test/resources/init.sql @@ -0,0 +1,352 @@ +-- ---------------------- 测试相关表及数据 ---------------------------- +-- 创建表 - 用户表 +create table KSA_SECURITY_USER ( + ID varchar(36) primary key, + NAME varchar(256), + PASSWORD varchar(256), + EMAIL varchar(256), + TELEPHONE varchar(256) +); + + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-1', '麻文强', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456' ); + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-2', '闫寅卓', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456' ); + +-- 创建表 - 基础数据 +create table KSA_BD_DATA ( + ID varchar(36) not null comment '标识', + CODE varchar(200) not null comment '编码', + NAME varchar(200) not null comment '名称', + ALIAS varchar(200) not null comment '别名', + NOTE varchar(2000) not null comment '备注', + EXTRA varchar(200) not null comment '附加属性', + TYPE_ID varchar(36) not null comment '类型标识', + RANK int not null comment '排序', + primary key ( ID ) +); + + -- 币种 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-RMB', 'RMB', '人民币', '', '', '1.000', '00-currency', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-USD', 'USD', '美元', '', '', '6.800', '00-currency', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-HKD', 'HKD', '港币', '', '', '0.882', '00-currency', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-JPY', 'JPY', '日元', '', '', '0.075', '00-currency', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-EUR', 'EUR', '欧元', '', '', '10.000', '00-currency', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-TWD', 'TWD', '台币', '', '', '0.200', '00-currency', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '00-currency-KRW', 'KRW', '韩元', '', '', '0.005', '00-currency', 7 ); + -- 费用类型 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-001', 'ABF', '安保费', '', '', '', '10-charge', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-002', 'AFC', 'Air/Ocean Freight Charge', '', '', '', '10-charge', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-003', 'AWC', 'Air Waybill Charge(AWC)', '', '', '', '10-charge', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '10-charge-004', 'BAF', 'BAF', '', '', '', '10-charge', 4 ); + -- 来往单位类型 + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-bgh', 'BGH','报关行', '', '', '', '20-department', 1 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-dls', 'DLS','代理商', '', '', '', '20-department', 2 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-gys', 'GYS','供应商', '', '', '', '20-department', 3 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-cd', 'CD','船代', '', '', '', '20-department', 4 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-cyr', 'CYR','承运人', '', '', '', '20-department', 5 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-chedui', 'CHEDUI','车队', '', '', '', '20-department', 6 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-gwdl', 'GWDL','国外代理', '', '', '', '20-department', 7 ); + insert into KSA_BD_DATA ( ID, CODE, NAME, ALIAS, NOTE, EXTRA, TYPE_ID, RANK ) values ( '20-department-hkgs', 'HKGS','航空公司', '', '', '', '20-department', 8 ); + +-- 创建表 - 合作伙伴数据 +create table KSA_BD_PARTNER ( + ID varchar(36) not null comment '标识' , + CODE varchar(200) not null comment '编码' , + NAME varchar(2000) not null comment '名称' , + ALIAS varchar(2000) not null comment '别名 - 主要显示在提单信息中' , + ADDRESS varchar(2000) not null comment '地址' , + PP int not null comment '付款周期 ( 天 )' , + NOTE varchar(2000) not null comment '备注' , + IMPORTANT int(1) default 0 not null comment '是否为重要伙伴' , + RANK int not null comment '排序' , + SALER_ID varchar(36) not null comment '销售担当标识', + primary key ( ID ), + unique ( CODE ) +); + + insert into KSA_BD_PARTNER ( ID, CODE, NAME, ALIAS, ADDRESS, PP, NOTE, IMPORTANT, RANK, SALER_ID ) values ( 'test-partner-1', 'test-partner-1', '测试合作伙伴1', 'Alias1-1', '', 30, '', 1, 1, 'test-user-1'); + insert into KSA_BD_PARTNER ( ID, CODE, NAME, ALIAS, ADDRESS, PP, NOTE, IMPORTANT, RANK, SALER_ID ) values ( 'test-partner-2', 'test-partner-2', '测试合作伙伴2', 'Alias2-1', '', 30, '', 0, 1001, '' ); + +-- ---------------------- 正式表与数据 ---------------------------- + +-- 创建表 - 托单表 +-- 创建表 - 托单表 +create table KSA_LOGISTICS_BOOKINGNOTE ( + ID varchar(36) not null comment '标识', + CODE varchar(200) not null comment '编号', + TYPE varchar(200) not null comment '托单类型', + TYPE_SUB varchar(200) comment '托单子类型(FCL/LCL)', + SERIAL_NUMBER int(10) not null comment '流水号', + CUSTOMER_ID varchar(36) not null comment '客户标识', + INVOICE_NUMBER varchar(200) comment '发票号', + CREATED_DATE date not null comment '接单日期', + CHARGE_DATE date comment '记账月份', + + SALER_ID varchar(36) comment '销售人员标识', + CREATOR_ID varchar(36) comment '创建人标识', + + CARRIER_ID varchar(200) comment '承运人标识 - 不强制外键关联', + SHIPPING_AGENT_ID varchar(200) comment '船代标识 - 不强制外键关联', + AGENT_ID varchar(200) comment '代理标识 - 不强制外键关联', + + CARGO_NAME varchar(200) comment '品名', + CARGO_NOTE varchar(2000) comment '货物备注', + CARGO_DESCRIPTION varchar(2000) comment '箱号封号', + CARGO_CONTAINER varchar(2000) comment '箱类箱型箱量描述', + SHIPPING_MARK varchar(200) comment '唛头', + VOLUMN numeric(10,3) comment '体积', + WEIGHT numeric(10,3) comment '毛重', + QUANTITY int(10) comment '数量', + UNIT varchar(200) comment '数量单位', + QUANTITY_DESCRIPTION varchar(2000) comment '英文数量描述', + + TITLE varchar(200) comment '开票抬头', + MAWB varchar(200) comment '主提单号', + HAWB varchar(200) comment '副提单号', + SHIPPER_ID varchar(36) comment '发货人标识', + CONSIGNEE_ID varchar(36) comment '收货人标识', + NOTIFY_ID varchar(36) comment '通知人标识', + + DEPARTURE varchar(200) comment '出发地', + DEPARTURE_PORT varchar(200) comment '出发港', + DEPARTURE_DATE date comment '出发日期', + DESTINATION varchar(200) comment '目的地', + DESTINATION_PORT varchar(200) comment '目的港', + DESTINATION_DATE date comment '到达日期', + LOADING_PORT varchar(200) comment '装货港/中转港口', + DISCHARGE_PORT varchar(200) comment '卸货港/中转港口', + DELIVER_DATE date comment '接收/派送日期', + + ROUTE varchar(200) comment '航线', + ROUTE_NAME varchar(200) comment '船名', + ROUTE_CODE varchar(200) comment '航次', + + CUSTOMS_BROKER_ID varchar(200) comment '代理标识 - 不强制外键关联', + CUSTOMS_CODE varchar(200) comment '报关单号', + CUSTOMS_DATE date comment '报关日期', + RETURN_CODE varchar(200) comment '退单单号', + RETURN_DATE date comment '退单日期', + RETURN_DATE2 date comment '退单日期2', + TAX_CODE varchar(200) comment '税单号', + TAX_DATE1 date comment '收税单日期', + TAX_DATE2 date comment '发税单日期', + EXPRESS_CODE varchar(200) comment '快递单号', + + VEHICLE_TEAM_ID varchar(200) comment '车队标识', + VEHICLE_TYPE varchar(200) comment '车型', + VEHICLE_NUMBER varchar(200) comment '车号', + + STATE int(8) not null comment '托单状态', + primary key ( ID ) +); + + insert into KSA_LOGISTICS_BOOKINGNOTE ( ID, CODE, TYPE, SERIAL_NUMBER, CUSTOMER_ID, INVOICE_NUMBER, CREATED_DATE, SALER_ID, CREATOR_ID, CARRIER_ID, SHIPPING_AGENT_ID, AGENT_ID, CARGO_NAME, CARGO_NOTE, CARGO_CONTAINER, CARGO_DESCRIPTION, SHIPPING_MARK, VOLUMN, WEIGHT, QUANTITY, UNIT,QUANTITY_DESCRIPTION, TITLE, MAWB, HAWB, SHIPPER_ID, CONSIGNEE_ID, NOTIFY_ID, DEPARTURE, DEPARTURE_PORT, DEPARTURE_DATE, DESTINATION, DESTINATION_PORT, DESTINATION_DATE, LOADING_PORT, DISCHARGE_PORT, DELIVER_DATE, ROUTE, ROUTE_NAME, ROUTE_CODE, CUSTOMS_BROKER_ID, CUSTOMS_CODE, CUSTOMS_DATE, RETURN_CODE, RETURN_DATE, RETURN_DATE2, TAX_CODE, TAX_DATE1, EXPRESS_CODE, TAX_DATE2, VEHICLE_TEAM_ID, VEHICLE_TYPE, VEHICLE_NUMBER, STATE ) values ( 'test-bookingnote-1', 'KHSE1', 'SE', 1, 'test-partner-1', '', '2012-10-1', 'test-user-1', 'test-user-2', '', '', '', '品名', '', '20GP*1+40GP*2', '', '', 123.0, 321.0, 222, '箱', '', '开票抬头', 'mawb', 'hawb', 'test-partner-1', 'test-partner-2', 'test-partner-2', '', '', '2012-1-1', '', '', '2012-2-2', '', '', '2012-3-3', '', '', '', 'test-partner-1', '', '2012-4-4', '', '2012-5-5', '2012-5-5', '', '2012-6-6', '', '2012-7-7', 'test-partner-2', '', '', 1 ); + insert into KSA_LOGISTICS_BOOKINGNOTE ( ID, CODE, TYPE, SERIAL_NUMBER, CUSTOMER_ID, INVOICE_NUMBER, CREATED_DATE, SALER_ID, CREATOR_ID, CARRIER_ID, SHIPPING_AGENT_ID, AGENT_ID, CARGO_NAME, CARGO_NOTE, CARGO_CONTAINER, CARGO_DESCRIPTION, SHIPPING_MARK, VOLUMN, WEIGHT, QUANTITY, UNIT,QUANTITY_DESCRIPTION, TITLE, MAWB, HAWB, SHIPPER_ID, CONSIGNEE_ID, NOTIFY_ID, DEPARTURE, DEPARTURE_PORT, DEPARTURE_DATE, DESTINATION, DESTINATION_PORT, DESTINATION_DATE, LOADING_PORT, DISCHARGE_PORT, DELIVER_DATE, ROUTE, ROUTE_NAME, ROUTE_CODE, CUSTOMS_BROKER_ID, CUSTOMS_CODE, CUSTOMS_DATE, RETURN_CODE, RETURN_DATE, RETURN_DATE2, TAX_CODE, TAX_DATE1, EXPRESS_CODE, TAX_DATE2, VEHICLE_TEAM_ID, VEHICLE_TYPE, VEHICLE_NUMBER, STATE ) values ( 'test-bookingnote-2', 'KHSI2', 'SI', 2, 'test-partner-1', '', '2012-10-1', 'test-user-1', 'test-user-2', '', '', '', '品名', '', '20GP*1+40GP*2', '', '', 123.0, 321.0, 222, '箱', '', '开票抬头', 'mawb', 'hawb', 'test-partner-1', 'test-partner-2', 'test-partner-2', '', '', '2012-1-1', '', '', '2012-2-2', '', '', '2012-3-3', '', '', '', 'test-partner-1', '', '2012-4-4', '', '2012-5-5', '2012-5-5', '', '2012-6-6', '', '2012-7-7', 'test-partner-2', '', '', 0 ); + insert into KSA_LOGISTICS_BOOKINGNOTE ( ID, CODE, TYPE, SERIAL_NUMBER, CUSTOMER_ID, INVOICE_NUMBER, CREATED_DATE, SALER_ID, CREATOR_ID, CARRIER_ID, SHIPPING_AGENT_ID, AGENT_ID, CARGO_NAME, CARGO_NOTE, CARGO_CONTAINER, CARGO_DESCRIPTION, SHIPPING_MARK, VOLUMN, WEIGHT, QUANTITY, UNIT,QUANTITY_DESCRIPTION, TITLE, MAWB, HAWB, SHIPPER_ID, CONSIGNEE_ID, NOTIFY_ID, DEPARTURE, DEPARTURE_PORT, DEPARTURE_DATE, DESTINATION, DESTINATION_PORT, DESTINATION_DATE, LOADING_PORT, DISCHARGE_PORT, DELIVER_DATE, ROUTE, ROUTE_NAME, ROUTE_CODE, CUSTOMS_BROKER_ID, CUSTOMS_CODE, CUSTOMS_DATE, RETURN_CODE, RETURN_DATE, RETURN_DATE2, TAX_CODE, TAX_DATE1, EXPRESS_CODE, TAX_DATE2, VEHICLE_TEAM_ID, VEHICLE_TYPE, VEHICLE_NUMBER, STATE ) values ( 'test-bookingnote-3', 'KHAE3', 'AE', 3, 'test-partner-1', '', '2012-10-1', 'test-user-1', 'test-user-2', '', '', '', '品名', '', '20GP*1+40GP*2', '', '', 123.0, 321.0, 222, '箱', '', '开票抬头', 'mawb', 'hawb', 'test-partner-1', 'test-partner-2', 'test-partner-2', '', '', '2012-1-1', '', '', '2012-2-2', '', '', '2012-3-3', '', '', '', 'test-partner-1', '', '2012-4-4', '', '2012-5-5', '2012-5-5', '', '2012-6-6', '', '2012-7-7', 'test-partner-2', '', '', 2 ); + insert into KSA_LOGISTICS_BOOKINGNOTE ( ID, CODE, TYPE, SERIAL_NUMBER, CUSTOMER_ID, INVOICE_NUMBER, CREATED_DATE, SALER_ID, CREATOR_ID, CARRIER_ID, SHIPPING_AGENT_ID, AGENT_ID, CARGO_NAME, CARGO_NOTE, CARGO_CONTAINER, CARGO_DESCRIPTION, SHIPPING_MARK, VOLUMN, WEIGHT, QUANTITY, UNIT,QUANTITY_DESCRIPTION, TITLE, MAWB, HAWB, SHIPPER_ID, CONSIGNEE_ID, NOTIFY_ID, DEPARTURE, DEPARTURE_PORT, DEPARTURE_DATE, DESTINATION, DESTINATION_PORT, DESTINATION_DATE, LOADING_PORT, DISCHARGE_PORT, DELIVER_DATE, ROUTE, ROUTE_NAME, ROUTE_CODE, CUSTOMS_BROKER_ID, CUSTOMS_CODE, CUSTOMS_DATE, RETURN_CODE, RETURN_DATE, RETURN_DATE2, TAX_CODE, TAX_DATE1, EXPRESS_CODE, TAX_DATE2, VEHICLE_TEAM_ID, VEHICLE_TYPE, VEHICLE_NUMBER, STATE ) values ( 'test-bookingnote-4', 'KHAI4', 'AI', 4, 'test-partner-1', '', '2012-10-1', 'test-user-1', 'test-user-2', '', '', '', '品名', '', '20GP*1+40GP*2', '', '', 123.0, 321.0, 222, '箱', '', '开票抬头', 'mawb', 'hawb', 'test-partner-1', 'test-partner-2', 'test-partner-2', '', '', '2012-1-1', '', '', '2012-2-2', '', '', '2012-3-3', '', '', '', 'test-partner-1', '', '2012-4-4', '', '2012-5-5', '2012-5-5', '', '2012-6-6', '', '2012-7-7', 'test-partner-2', '', '', 8 ); + insert into KSA_LOGISTICS_BOOKINGNOTE ( ID, CODE, TYPE, SERIAL_NUMBER, CUSTOMER_ID, INVOICE_NUMBER, CREATED_DATE, SALER_ID, CREATOR_ID, CARRIER_ID, SHIPPING_AGENT_ID, AGENT_ID, CARGO_NAME, CARGO_NOTE, CARGO_CONTAINER, CARGO_DESCRIPTION, SHIPPING_MARK, VOLUMN, WEIGHT, QUANTITY, UNIT,QUANTITY_DESCRIPTION, TITLE, MAWB, HAWB, SHIPPER_ID, CONSIGNEE_ID, NOTIFY_ID, DEPARTURE, DEPARTURE_PORT, DEPARTURE_DATE, DESTINATION, DESTINATION_PORT, DESTINATION_DATE, LOADING_PORT, DISCHARGE_PORT, DELIVER_DATE, ROUTE, ROUTE_NAME, ROUTE_CODE, CUSTOMS_BROKER_ID, CUSTOMS_CODE, CUSTOMS_DATE, RETURN_CODE, RETURN_DATE, RETURN_DATE2, TAX_CODE, TAX_DATE1, EXPRESS_CODE, TAX_DATE2, VEHICLE_TEAM_ID, VEHICLE_TYPE, VEHICLE_NUMBER, STATE ) values ( 'test-bookingnote-5', 'KHSI5', 'SI', 5, 'test-partner-1', '', '2012-10-1', 'test-user-1', 'test-user-2', '', '', '', '品名', '', '20GP*1+40GP*2', '', '', 123.0, 321.0, 222, '箱', '', '开票抬头', 'mawb', 'hawb', 'test-partner-1', 'test-partner-2', 'test-partner-2', '', '', '2012-1-1', '', '', '2012-2-2', '', '', '2012-3-3', '', '', '', 'test-partner-1', '', '2012-4-4', '', '2012-5-5', '2012-5-5', '', '2012-6-6', '', '2012-7-7', 'test-partner-2', '', '', -1 ); + + + +-- 创建表 - 托单货物表 +create table KSA_LOGISTICS_BOOKINGNOTE_CARGO ( + ID varchar(36) not null comment '货物标识', + NAME varchar(200) not null comment '货物名称', + CATEGORY varchar(200) not null comment '箱类', + TYPE varchar(200) not null comment '箱型', + AMOUNT int(10) not null comment '箱量', + BOOKINGNOTE_ID varchar(36) not null comment '所属托单标识', + primary key ( ID ), + constraint FK_KSA_LOGISTICS_BOOKINGNOTE_CARGO + foreign key ( BOOKINGNOTE_ID ) + references KSA_LOGISTICS_BOOKINGNOTE ( ID ) + on delete cascade + on update cascade +); + + +-- 创建表 - 提单确认书 +create table KSA_LOGISTICS_BILLOFLADING ( + ID varchar(36) comment '单据标识', + TARGET varchar(200) comment '提单对象', + PUBLISH_DATE varchar(200) comment '发布日期', + SHIPPER varchar(200) comment '发货人名称', + CONSIGNEE varchar(200) comment '收货人名称', + NOTIFY varchar(200) comment '通知人名称', + CODE varchar(200) comment '提单编号', + DELIVER_TYPE varchar(200) comment '发送方式: 电放or正本', + CUSTOMER_CODE varchar(200) comment '客户编号', + SELF_CODE varchar(200) comment '我司编号', + CREATOR varchar(200) comment '发件人', + BILL_TYPE varchar(200) comment '提单类型', + NOTE varchar(200) comment '备注', + AGENT varchar(200) comment '海外代理', + VESSEL_VOYAGE varchar(200) comment '船名航次', + LOADING_PORT varchar(200) comment '装货港', + DISCHARGE_PORT varchar(200) comment '卸货港', + DESTINATION_PORT varchar(200) comment '目的港', + CARGO_MARK varchar(200) comment '货物标记', + CARGO_QUANTITY varchar(200) comment '货物数量', + CARGO_NAME varchar(200) comment '货物名称', + CARGO_WEIGHT varchar(200) comment '货物毛重', + CARGO_VOLUMN varchar(200) comment '货物体积', + CARGO_DESCRIPTION varchar(200) comment '箱号封号', + CARGO_QUANTITY_DESCRIPTION varchar(200) comment '货物英文数量描述', + PAY_MODE varchar(50) comment '付款方式', + BOOKINGNOTE_ID varchar(36) not null comment '所属托单标识', + primary key ( ID ), + constraint KSA_LOGISTICS_BILLOFLADING + foreign key ( BOOKINGNOTE_ID ) + references KSA_LOGISTICS_BOOKINGNOTE ( ID ) + on delete cascade + on update cascade +); + +-- 创建表 - 到货通知单 +create table KSA_LOGISTICS_ARRIVALNOTE ( + ID varchar(36) comment '单据标识', + ARRIVAL_DATE varchar(200) comment '到货日期', + CODE varchar(200) comment '编号', + SHIPPER varchar(200) comment '发货人名称', + CONSIGNEE varchar(200) comment '收货人名称', + + VESSEL varchar(200) comment '船名', + VOYAGE varchar(200) comment '航次', + MAWB varchar(200) comment '主单号', + HAWB varchar(200) comment '副单号', + CONTAINER varchar(200) comment '箱号封号', + SEAL varchar(200) comment '封印', + ETA varchar(200) comment '到货日', + + CY varchar(200) comment 'cy or cfs', + LOADING_PORT varchar(200) comment '装货港', + DISCHARGE_PORT varchar(200) comment '卸货港', + DELIVER_PLACE varchar(200) comment '送货地', + + CARGO_MARK varchar(200) comment '货物标记', + CARGO_WEIGHT varchar(200) comment '货物毛重', + CARGO_VOLUMN varchar(200) comment '货物体积', + CARGO_DESCRIPTION varchar(200) comment '货物描述', + CARGO varchar(200) comment '货物数量', + CARGO_PKG varchar(200) comment 'pkg', + CARGO_COUNT varchar(200) comment 'count', + + FREIGHT varchar(200) comment 'FREIGHT', + CHARGE varchar(200) comment 'CHARGE', + RATE varchar(200) comment 'RATE', + + BOOKINGNOTE_ID varchar(36) not null comment '所属托单标识', + primary key ( ID ), + constraint KSA_LOGISTICS_ARRIVALNOTE + foreign key ( BOOKINGNOTE_ID ) + references KSA_LOGISTICS_BOOKINGNOTE ( ID ) + on delete cascade + on update cascade +); + +-- 创建表 - 进仓通知单 +create table KSA_LOGISTICS_WAREHOUSENOTING ( + ID varchar(36) comment '单据标识', + SALER varchar(200) comment '销售担当', + TARGET varchar(200) comment '提单对象', + CODE varchar(200) comment '提单编号', + CREATED_DATE varchar(200) comment '进仓时间', + CARGO_NAME varchar(200) comment '货物名称', + CARGO_WEIGHT varchar(200) comment '货物毛重', + CARGO_VOLUMN varchar(200) comment '货物体积', + CARGO_QUANTITY varchar(200) comment '货物数量', + CUSTOMER varchar(200) comment '委托客户', + LOADING_PORT varchar(200) comment '装货港', + DISCHARGE_PORT varchar(200) comment '卸货港', + VESSEL_VOYAGE varchar(200) comment '船名航次', + DESTINATION varchar(200) comment '目的地', + DEPARTURE_DATE varchar(200) comment '出航日', + MAWB varchar(200) comment '提单号', + ENTRY_DATE varchar(200) comment '最晚入仓时间', + INFORM_DATE varchar(200) comment '通知时间', + ADDRESS varchar(200) comment '进仓地址', + CONTACT varchar(200) comment '联系人', + TELEPHONE varchar(200) comment '电话', + FAX varchar(200) comment '传真', + BOOKINGNOTE_ID varchar(36) not null comment '所属托单标识', + SALER_TEL varchar(200) comment '销售电话', + SALER_FAX varchar(200) comment '销售传真', + SALER_EMAIL varchar(200) comment '销售邮件', + primary key ( ID ), + constraint KSA_LOGISTICS_WAREHOUSENOTING + foreign key ( BOOKINGNOTE_ID ) + references KSA_LOGISTICS_BOOKINGNOTE ( ID ) + on delete cascade + on update cascade +); + +-- 创建表 - 订舱通知单 +create table KSA_LOGISTICS_WAREHOUSEBOOKING ( + ID varchar(36) comment '单据标识', + SALER varchar(200) comment '销售担当', + SHIPPER varchar(200) comment '发货人名称', + CONSIGNEE varchar(200) comment '收货人名称', + NOTIFY varchar(200) comment '通知人名称', + CODE varchar(200) comment '提单编号', + CREATED_DATE varchar(200) comment '进仓时间', + DEPARTURE_PORT varchar(200) comment '起运港', + DESTINATION_PORT varchar(200) comment '目的港', + + SWITCH_SHIP varchar(200) comment '转船', + GROUPING varchar(200) comment '分批', + TRANSPORT_MODE varchar(200) comment '运输方式', + PAYMENT_MODE varchar(200) comment '付款方式', + FREIGHT_CHARGE varchar(200) comment '运费', + + CARGO_CONTAINER varchar(200) comment '箱量', + SHIPPING_MARK varchar(200) comment '唛头', + CARGO_NAME varchar(200) comment '货物名称', + CARGO_WEIGHT varchar(200) comment '货物毛重', + CARGO_VOLUMN varchar(200) comment '货物体积', + CARGO_QUANTITY varchar(200) comment '货物数量', + TOTAL_WEIGHT varchar(200) comment '汇总毛重', + TOTAL_VOLUMN varchar(200) comment '汇总体积', + TOTAL_QUANTITY varchar(200) comment '汇总数量', + NOTE varchar(2000) comment '注意事项', + BOOKINGNOTE_ID varchar(36) not null comment '所属托单标识', + SALER_TEL varchar(200) comment '销售电话', + SALER_FAX varchar(200) comment '销售传真', + SALER_EMAIL varchar(200) comment '销售邮件', + primary key ( ID ), + constraint KSA_LOGISTICS_WAREHOUSEBOOKING + foreign key ( BOOKINGNOTE_ID ) + references KSA_LOGISTICS_BOOKINGNOTE ( ID ) + on delete cascade + on update cascade +); + +-- 创建表 - MANIFEST +create table KSA_LOGISTICS_MANIFEST ( + ID varchar(36) comment '单据标识', + SALER varchar(200) comment '销售担当', + CODE varchar(200) comment '提单编号', + LOADING_PORT varchar(200) comment '装货港', + DESTINATION_PORT varchar(200) comment '目的港', + FLIGHT_DATE varchar(200) comment '航班和时间', + AGENT varchar(200) comment '代理商', + + HAWB varchar(200) comment 'hawb', + CARGO_NAME varchar(200) comment '货物名称', + CARGO_WEIGHT varchar(200) comment '货物毛重', + FINAL_DESTINATION varchar(200) comment '最终目的地', + SHIPPER varchar(200) comment '发货人名称', + CONSIGNEE varchar(200) comment '收货人名称', + RE varchar(200) comment 're', + + TOTAL_HAWB varchar(200) comment 'total-hawb', + TOTAL_PACKAGES varchar(200) comment 'total-packages', + BOOKINGNOTE_ID varchar(36) not null comment '所属托单标识', + SALER_TEL varchar(200) comment '销售电话', + SALER_FAX varchar(200) comment '销售传真', + SALER_EMAIL varchar(200) comment '销售邮件', + primary key ( ID ), + constraint KSA_LOGISTICS_MANIFEST + foreign key ( BOOKINGNOTE_ID ) + references KSA_LOGISTICS_BOOKINGNOTE ( ID ) + on delete cascade + on update cascade +); \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/pom.xml b/test_input/ksa/ksa-dao-root/ksa-security-dao/pom.xml new file mode 100644 index 0000000..b8eec5a --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.ksa + ksa-dao-root + 3.9.0 + + + ksa-security-dao + jar + + ksa-security-dao + 杭州凯思爱物流管理系统 - 安全管理 DAO 模块 + + + UTF-8 + + diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisPermissionDao.java b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisPermissionDao.java new file mode 100644 index 0000000..c10affb --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisPermissionDao.java @@ -0,0 +1,32 @@ +package com.ksa.dao.security.mybatis; + +import java.util.List; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.security.PermissionDao; +import com.ksa.model.security.Permission; + +/** + * 基于 Mybaits 的 PermissionDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisPermissionDao extends AbstractMybatisDao implements PermissionDao { + + @Override + public List selectAllPermission() throws RuntimeException { + return this.session.selectList( "select-security-permission-all" ); + } + + @Override + public Permission selectPermissionById( String id ) throws RuntimeException { + return this.session.selectOne( "select-security-permission-byid", id ); + } + + @Override + public List selectPermissionByRoleId( String roleId ) throws RuntimeException { + return this.session.selectList( "select-security-permission-byroleid", roleId ); + } +} diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisRoleDao.java b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisRoleDao.java new file mode 100644 index 0000000..28ab4f5 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisRoleDao.java @@ -0,0 +1,61 @@ +package com.ksa.dao.security.mybatis; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.security.RoleDao; +import com.ksa.model.security.Role; + +/** + * 基于 Mybaits 的 RoleDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisRoleDao extends AbstractMybatisDao implements RoleDao { + + @Override + public int insertRole( Role role ) throws RuntimeException { + return this.session.insert( "insert-security-role", role ); + } + + @Override + public int updateRole( Role role ) throws RuntimeException { + return this.session.update( "update-security-role", role ); + } + + @Override + public int deleteRole( Role role ) throws RuntimeException { + return this.session.delete( "delete-security-role", role ); + } + + @Override + public List selectAllRole() throws RuntimeException { + return this.session.selectList( "select-security-role-all" ); + } + + @Override + public Role selectRoleById( String id ) throws RuntimeException { + return this.session.selectOne( "select-security-role-byid", id ); + } + + @Override + public int insertRolePermission( String roleId, String permissionId ) throws RuntimeException { + Map params = new HashMap(); + params.put( "roleId", roleId ); + params.put( "permissionId", permissionId ); + return this.session.insert( "insert-security-rolepermission", params ); + } + + @Override + public int deleteRolePermission( String roleId, String permissionId ) throws RuntimeException { + Map params = new HashMap(); + params.put( "roleId", roleId ); + params.put( "permissionId", permissionId ); + return this.session.delete( "delete-security-rolepermission", params ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisUserDao.java b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisUserDao.java new file mode 100644 index 0000000..f335da5 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/java/com/ksa/dao/security/mybatis/MybatisUserDao.java @@ -0,0 +1,61 @@ +package com.ksa.dao.security.mybatis; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.ksa.dao.AbstractMybatisDao; +import com.ksa.dao.security.UserDao; +import com.ksa.model.security.User; + +/** + * 基于 Mybaits 的 UserDao 实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class MybatisUserDao extends AbstractMybatisDao implements UserDao { + + @Override + public int insertUser( User user ) throws RuntimeException { + return this.session.insert( "insert-security-user", user ); + } + + @Override + public int updateUser( User user ) throws RuntimeException { + return this.session.update( "update-security-user", user ); + } + + @Override + public int deleteUser( User user ) throws RuntimeException { + return this.session.delete( "delete-security-user", user ); + } + + @Override + public List selectAllUser() throws RuntimeException { + return this.session.selectList( "select-security-user-all" ); + } + + @Override + public User selectUserById( String id ) throws RuntimeException { + return this.session.selectOne( "select-security-user-byid", id ); + } + + @Override + public int insertUserRole( String userId, String roleId ) throws RuntimeException { + Map params = new HashMap(); + params.put( "userId", userId ); + params.put( "roleId", roleId ); + return this.session.insert( "insert-security-userrole", params ); + } + + @Override + public int deleteUserRole( String userId, String roleId ) throws RuntimeException { + Map params = new HashMap(); + params.put( "userId", userId ); + params.put( "roleId", roleId ); + return this.session.delete( "delete-security-userrole", params ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-permission.xml b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-permission.xml new file mode 100644 index 0000000..2634012 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-permission.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + SELECT ID, NAME, DESCRIPTION + FROM KSA_SECURITY_PERMISSION + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-role.xml b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-role.xml new file mode 100644 index 0000000..84ab24b --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-role.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_SECURITY_ROLE + ( ID, NAME, DESCRIPTION ) + VALUES ( #{id}, #{name,jdbcType=VARCHAR}, #{description,jdbcType=VARCHAR} ) + + + + + UPDATE KSA_SECURITY_ROLE SET + NAME = #{name,jdbcType=VARCHAR}, + DESCRIPTION = #{description,jdbcType=VARCHAR} + WHERE ID = #{id} + + + + + DELETE FROM KSA_SECURITY_ROLE WHERE ID = #{id} + + + + SELECT ID, NAME, DESCRIPTION + FROM KSA_SECURITY_ROLE + + + + + + + + + + + + + + INSERT INTO KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) + VALUES ( #{roleId}, #{permissionId} ) + + + + DELETE FROM KSA_SECURITY_ROLEPERMISSION + WHERE ROLE_ID = #{roleId} AND PERMISSION_ID = #{permissionId} + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-user.xml b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-user.xml new file mode 100644 index 0000000..3b6da9b --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/mybatis/mapper/security-user.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO KSA_SECURITY_USER + ( ID, NAME, PASSWORD, EMAIL, TELEPHONE, IS_LOCKED ) + VALUES ( #{id}, #{name,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{telephone,jdbcType=VARCHAR}, #{locked, jdbcType=NUMERIC} ) + + + + + UPDATE KSA_SECURITY_USER SET + NAME = #{name,jdbcType=VARCHAR}, + PASSWORD = #{password,jdbcType=VARCHAR}, + EMAIL = #{email,jdbcType=VARCHAR}, + TELEPHONE = #{telephone,jdbcType=VARCHAR}, + IS_LOCKED = #{locked, jdbcType=NUMERIC} + WHERE ID = #{id} + + + + + DELETE FROM KSA_SECURITY_USER WHERE ID = #{id} + + + + + + + + + + + INSERT INTO KSA_SECURITY_USERROLE ( USER_ID, ROLE_ID ) + VALUES ( #{userId}, #{roleId} ) + + + + DELETE FROM KSA_SECURITY_USERROLE + WHERE USER_ID = #{userId} AND ROLE_ID = #{roleId} + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/spring/dao/security-dao-context.xml b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/spring/dao/security-dao-context.xml new file mode 100644 index 0000000..3c89acf --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/main/resources/spring/dao/security-dao-context.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisRoleDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisRoleDaoTest.java new file mode 100644 index 0000000..5d785ff --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisRoleDaoTest.java @@ -0,0 +1,81 @@ +package com.ksa.dao.security.mybatis; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.security.RoleDao; +import com.ksa.model.security.Permission; +import com.ksa.model.security.Role; + +public class MybatisRoleDaoTest extends MybatisDaoTest { + + @Test + public void testSelectRoleById() throws RuntimeException { + RoleDao dao = CONTEXT.getBean( "roleDao", RoleDao.class ); + Role role = dao.selectRoleById( "test-role-1" ); + Assert.assertEquals( "系统管理员", role.getName() ); + Assert.assertEquals( "系统管理员描述", role.getDescription() ); + + Permission[] ps = role.getPermissions(); + Assert.assertNotNull( ps ); + Assert.assertEquals( 4, ps.length ); + } + + @Test + public void testSelectAllRole() throws RuntimeException { + RoleDao dao = CONTEXT.getBean( "roleDao", RoleDao.class ); + List list = dao.selectAllRole(); + Assert.assertEquals( 2, list.size() ); + for( Role r : list ) { + Assert.assertNull( r.getPermissions() ); + } + } + + @Test + public void testCrudRole() throws RuntimeException { + RoleDao dao = CONTEXT.getBean( "roleDao", RoleDao.class ); + + Role role = new Role(); + role.setId( "custom-role" ); + role.setName( "角色" ); + role.setDescription( "角色描述" ); + + // 测试插入是否成功 + dao.insertRole( role ); + Role temp = dao.selectRoleById( "custom-role" ); + Assert.assertEquals( "角色", temp.getName() ); + Assert.assertEquals( "角色描述", temp.getDescription() ); + + role.setName( "角色1" ); + role.setDescription( "角色描述1" ); + + // 测试更新是否成功 + dao.updateRole( role ); + Role temp2 = dao.selectRoleById( "custom-role" ); + Assert.assertEquals( "角色1", temp2.getName() ); + Assert.assertEquals( "角色描述1", temp2.getDescription() ); + + // 测试权限的增删 + dao.insertRolePermission( "custom-role", "bookingnote:edit" ); + dao.insertRolePermission( "custom-role", "bookingnote:delete" ); + temp = dao.selectRoleById( "custom-role" ); + Permission[] ps = temp.getPermissions(); + Assert.assertNotNull( ps ); + Assert.assertEquals( 2, ps.length ); + + dao.deleteRolePermission( "custom-role", "bookingnote:edit" ); + temp = dao.selectRoleById( "custom-role" ); + ps = temp.getPermissions(); + Assert.assertNotNull( ps ); + Assert.assertEquals( 1, ps.length ); + + // 测试删除 + dao.deleteRole( role ); + Role temp3 = dao.selectRoleById( "custom-id" ); + Assert.assertNull( temp3 ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisUserDaoTest.java b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisUserDaoTest.java new file mode 100644 index 0000000..a92ce16 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/java/com/ksa/dao/security/mybatis/MybatisUserDaoTest.java @@ -0,0 +1,74 @@ +package com.ksa.dao.security.mybatis; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import com.ksa.dao.MybatisDaoTest; +import com.ksa.dao.security.UserDao; +import com.ksa.model.security.User; + +public class MybatisUserDaoTest extends MybatisDaoTest { + + @Test + public void testSelectUserById() throws RuntimeException { + UserDao dao = CONTEXT.getBean( "userDao", UserDao.class ); + User user = dao.selectUserById( "test-user-1" ); + Assert.assertEquals( "麻文强", user.getName() ); + Assert.assertEquals( "a@a.a", user.getEmail() ); + Assert.assertEquals( "123456", user.getTelephone() ); + Assert.assertEquals( "fEqNCco3Yq9h5ZUglD3CZJT4lBs", user.getPassword() ); + Assert.assertFalse( user.isLocked() ); + } + + @Test + public void testSelectAllUser() throws RuntimeException { + UserDao dao = CONTEXT.getBean( "userDao", UserDao.class ); + List list = dao.selectAllUser(); + Assert.assertEquals( 2, list.size() ); + } + + @Test + public void testCrudUser() throws RuntimeException { + UserDao dao = CONTEXT.getBean( "userDao", UserDao.class ); + + User user = new User(); + user.setId( "custom-id" ); + user.setName( "麻文强" ); + user.setPassword( "333" ); + user.setEmail( "b@b.b" ); + user.setTelephone( "444" ); + user.setLocked( false ); + + // 测试插入是否成功 + dao.insertUser( user ); + User temp = dao.selectUserById( "custom-id" ); + Assert.assertEquals( "麻文强", temp.getName() ); + Assert.assertEquals( "b@b.b", temp.getEmail() ); + Assert.assertEquals( "444", temp.getTelephone() ); + Assert.assertEquals( "333", temp.getPassword() ); + Assert.assertFalse( temp.isLocked() ); + + user.setName( "强文麻" ); + user.setPassword( "555" ); + user.setEmail( "c@c.c" ); + user.setTelephone( "666" ); + user.setLocked( true ); + + // 测试更新是否成功 + dao.updateUser( user ); + User temp2 = dao.selectUserById( "custom-id" ); + Assert.assertEquals( "强文麻", temp2.getName() ); + Assert.assertEquals( "c@c.c", temp2.getEmail() ); + Assert.assertEquals( "666", temp2.getTelephone() ); + Assert.assertEquals( "555", temp2.getPassword() ); + Assert.assertTrue( temp2.isLocked() ); + + // 测试删除 + dao.deleteUser( user ); + User temp3 = dao.selectUserById( "custom-id" ); + Assert.assertNull( temp3 ); + } + +} diff --git a/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/resources/init.sql b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/resources/init.sql new file mode 100644 index 0000000..54bfa81 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/ksa-security-dao/src/test/resources/init.sql @@ -0,0 +1,88 @@ +-- 创建表 - 用户表 +create table KSA_SECURITY_USER ( + ID varchar(36) not null comment '用户标识' , + NAME varchar(200) not null comment '用户姓名' , + PASSWORD varchar(200) not null comment '登录密码' , + EMAIL varchar(200) not null comment '用户邮箱' , + TELEPHONE varchar(200) not null comment '用户电话' , + IS_LOCKED int(1) default 0 not null comment '是否锁定' , + primary key ( ID ) +); + + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-1', '麻文强', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456'); + insert into KSA_SECURITY_USER ( ID, NAME, PASSWORD, EMAIL, TELEPHONE ) values ( 'test-user-2', '闫寅卓', 'fEqNCco3Yq9h5ZUglD3CZJT4lBs', 'a@a.a', '123456' ); + +-- 创建表 - 角色表 +create table KSA_SECURITY_ROLE ( + ID varchar(36) not null comment '角色标识' , + NAME varchar(200) not null comment '角色名称' , + DESCRIPTION varchar(2000) not null comment '角色描述' , + primary key ( ID ) +); + + insert into KSA_SECURITY_ROLE ( ID, NAME, DESCRIPTION ) values ( 'test-role-1', '系统管理员', '系统管理员描述' ); + insert into KSA_SECURITY_ROLE ( ID, NAME, DESCRIPTION ) values ( 'test-role-2', '操作员', '操作员描述' ); + +-- 创建表 - 权限表 +create table KSA_SECURITY_PERMISSION ( + ID varchar(200) not null comment '权限标识', + NAME varchar(200) not null comment '权限名称', + DESCRIPTION varchar(2000) not null comment '权限描述', + primary key ( ID ) +); + + insert into KSA_SECURITY_PERMISSION ( ID, NAME, DESCRIPTION ) values ( 'bookingnote:edit:view', '托单查看', '可以查看所有的业务托单,但是并没有编辑的权限。' ); + insert into KSA_SECURITY_PERMISSION ( ID, NAME, DESCRIPTION ) values ( 'bookingnote:print', '托单打印', '可以打印提单、订舱通知等业务相关的文档。' ); + insert into KSA_SECURITY_PERMISSION ( ID, NAME, DESCRIPTION ) values ( 'bookingnote:edit', '托单编辑', '可以新增和修改所有的业务托单,但是没有删除的权限。' ); + insert into KSA_SECURITY_PERMISSION ( ID, NAME, DESCRIPTION ) values ( 'bookingnote:delete', '托单删除', '可以删除托单(并非真正删除,而是将托单置于已删除状态)。' ); + +-- 创建表 - 用户角色关联表 +create table KSA_SECURITY_USERROLE ( + USER_ID varchar(36) not null comment '用户标识:外键关联 KSA_SECURITY_USER 表中的 ID 字段' , + ROLE_ID varchar(36) not null comment '角色标识:外键关联 KSA_SECURITY_ROLE 表中的 ID 字段' , + + primary key (USER_ID, ROLE_ID) , + + constraint FK_KSA_SECURITY_USERROLE + foreign key ( USER_ID ) + references KSA_SECURITY_USER ( ID ) + on delete cascade + on update cascade , + + constraint FK_KSA_SECURITY_ROLEUSER + foreign key ( ROLE_ID ) + references KSA_SECURITY_ROLE ( ID ) + on delete cascade + on update cascade +); + + insert into KSA_SECURITY_USERROLE ( USER_ID, ROLE_ID ) values ( 'test-user-1', 'test-role-1' ); + insert into KSA_SECURITY_USERROLE ( USER_ID, ROLE_ID ) values ( 'test-user-1', 'test-role-2' ); + insert into KSA_SECURITY_USERROLE ( USER_ID, ROLE_ID ) values ( 'test-user-2', 'test-role-2' ); + + +-- 创建表 - 角色权限关联表 +CREATE TABLE KSA_SECURITY_ROLEPERMISSION ( + ROLE_ID varchar(36) not null comment '角色标识:外键关联 KSA_SECURITY_ROLE 表中的 ID 字段' , + PERMISSION_ID varchar(36) not null comment '权限标识:外键关联 KSA_SECURITY_PERMISSION 表中的 ID 字段' , + + primary key ( ROLE_ID, PERMISSION_ID ) , + + constraint FK_KSA_SECURITY_ROLEPERMISSION + foreign key ( ROLE_ID ) + references KSA_SECURITY_ROLE ( ID ) + on delete cascade + on update cascade , + + constraint FK_KSA_SECURITY_PERMISSIONROLE + foreign key ( PERMISSION_ID ) + references KSA_SECURITY_PERMISSION ( ID ) + on delete cascade + on update cascade +); + + insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'test-role-1', 'bookingnote:edit:view' ); + insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'test-role-1', 'bookingnote:print' ); + insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'test-role-1', 'bookingnote:edit' ); + insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'test-role-1', 'bookingnote:delete' ); + insert into KSA_SECURITY_ROLEPERMISSION ( ROLE_ID, PERMISSION_ID ) values ( 'test-role-2', 'bookingnote:edit:view' ); \ No newline at end of file diff --git a/test_input/ksa/ksa-dao-root/pom.xml b/test_input/ksa/ksa-dao-root/pom.xml new file mode 100644 index 0000000..308f389 --- /dev/null +++ b/test_input/ksa/ksa-dao-root/pom.xml @@ -0,0 +1,47 @@ + + 4.0.0 + + + com.ksa + ksa-root + 3.9.0 + + + ksa-dao-root + pom + + ksa-dao-root + 杭州凯思爱物流管理系统 - DAO 模块根模型 + + + UTF-8 + + + + + com.ksa + ksa-core + ${project.version} + + + com.ksa + ksa-dao-context + ${project.version} + + + + com.ksa + ksa-debug + ${project.version} + test + + + + + ksa-security-dao + ksa-bd-dao + ksa-logistics-dao + ksa-finance-dao + + diff --git a/test_input/ksa/ksa-debug/pom.xml b/test_input/ksa/ksa-debug/pom.xml new file mode 100644 index 0000000..7b21cee --- /dev/null +++ b/test_input/ksa/ksa-debug/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + + + com.ksa + ksa-root + 3.9.0 + + + ksa-debug + jar + + ksa-debug + 杭州凯思爱物流管理系统 - 调试模块 + + + UTF-8 + + + + + commons-dbcp + commons-dbcp + + + + + com.h2database + h2 + + + mysql + mysql-connector-java + 5.1.18 + + + + + + + org.springframework + spring-context + + + + + org.freemarker + freemarker + 2.3.18 + + + + + junit + junit + 4.8.2 + + + diff --git a/test_input/ksa/ksa-debug/src/main/java/com/ksa/dao/MybatisDaoTest.java b/test_input/ksa/ksa-debug/src/main/java/com/ksa/dao/MybatisDaoTest.java new file mode 100644 index 0000000..19fd46f --- /dev/null +++ b/test_input/ksa/ksa-debug/src/main/java/com/ksa/dao/MybatisDaoTest.java @@ -0,0 +1,19 @@ +package com.ksa.dao; + +import org.junit.BeforeClass; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + + +public abstract class MybatisDaoTest { + + public static final String[] PATHS = { "classpath:test/mybatis-test-context.xml" }; + + protected static ApplicationContext CONTEXT; + + @BeforeClass + public static void init() { + // 初始化应用环境 + CONTEXT = new ClassPathXmlApplicationContext( PATHS ); + } +} diff --git a/test_input/ksa/ksa-debug/src/main/java/com/ksa/freemarker/TemplateTest.java b/test_input/ksa/ksa-debug/src/main/java/com/ksa/freemarker/TemplateTest.java new file mode 100644 index 0000000..4130390 --- /dev/null +++ b/test_input/ksa/ksa-debug/src/main/java/com/ksa/freemarker/TemplateTest.java @@ -0,0 +1,33 @@ +package com.ksa.freemarker; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import org.junit.BeforeClass; + +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.Template; + + +public abstract class TemplateTest { + + protected static Configuration CONFIG; + + @BeforeClass + public static void init() { + // 初始化应用环境 + CONFIG = new Configuration(); + CONFIG.setObjectWrapper(new DefaultObjectWrapper()); + CONFIG.setClassForTemplateLoading( TemplateTest.class, "/" ); + } + + protected Writer getDefaultOutputWriter() { + return new OutputStreamWriter( System.out ); + } + + protected Template getTemplate( String name ) throws IOException { + return CONFIG.getTemplate( name, "UTF-8" ); + } +} diff --git a/test_input/ksa/ksa-debug/src/main/java/com/ksa/h2/H2DataSourceFactoryBean.java b/test_input/ksa/ksa-debug/src/main/java/com/ksa/h2/H2DataSourceFactoryBean.java new file mode 100644 index 0000000..85771ea --- /dev/null +++ b/test_input/ksa/ksa-debug/src/main/java/com/ksa/h2/H2DataSourceFactoryBean.java @@ -0,0 +1,197 @@ +package com.ksa.h2; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.List; + +import javax.sql.DataSource; + +import org.h2.jdbcx.JdbcConnectionPool; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; + +public class H2DataSourceFactoryBean implements FactoryBean, InitializingBean { + + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + protected String url; + + protected String username; + + protected String password; + + protected List initSqlScripts; + + @Override + public synchronized void afterPropertiesSet() throws Exception { + if( this.initSqlScripts != null && this.initSqlScripts.size() > 0 ) { + StringBuilder sb = new StringBuilder( this.url ); + File scriptDir = getWorkingDir(); + for( Resource res : initSqlScripts ) { + // 将初始化脚本转换成本地文件。 + File script = toLocalFile( res, scriptDir ); + if( sb.length() == this.url.length() ) { + // 第一个初始化文件, 其中【;】是 H2 数据源连接字符串中,附加参数的分隔符。 */ + sb.append( ";INIT=" ); //$NON-NLS-1$ + } else { + // 第二个到最后一个初始化文件,其中【\\;】是多个初始化SQL脚本文件之间的分隔符。 + sb.append( "\\;" ); //$NON-NLS-1$ + } + + sb.append( "RUNSCRIPT FROM '" ) //$NON-NLS-1$ + .append( script.getAbsolutePath().replace( '\\', '/' ) ) //$NON-NLS-1$ //$NON-NLS-2$ + .append( "'" ); //$NON-NLS-1$ + } + this.url = sb.toString(); + + this.ds = JdbcConnectionPool.create( getUrl(), getUsername(), getPassword() ); + } + } + + private DataSource ds; + + @Override + public DataSource getObject() throws Exception { + return this.ds; + } + + @Override + public Class getObjectType() { + return DataSource.class; + } + + @Override + public final boolean isSingleton() { + return true; + } + + /** + * 返回 H2 数据源连接字符串。 + * + * @return 建立 H2 数据源所需的连接字符串 + */ + public synchronized String getUrl() { + return this.url; + } + + /** + * 设置 H2 数据源连接字符串。 + * + * @param url + * 建立 H2 数据源所需的连接字符串 + */ + public synchronized void setUrl( String url ) { + this.url = url; + } + + /** + * 返回 H2 数据源连接的用户名。 + * + * @return 建立 H2 数据源所需的用户名 + */ + public String getUsername() { + if( username == null ) { + return ""; //$NON-NLS-1$ + } + return username; + } + + /** + * 设置 H2 数据源连接的用户名。 + * + * @param username + * 建立 H2 数据源所需的用户名 + */ + public void setUsername( String username ) { + this.username = username; + } + + /** + * 返回 H2 数据源连接的密码。 + * + * @return 建立 H2 数据源所需的密码 + */ + public String getPassword() { + if( password == null ) { + return ""; //$NON-NLS-1$ + } + return password; + } + + /** + * 设置 H2 数据源连接密码。 + * + * @param password + * 建立 H2 数据源所需的连接密码 + */ + public void setPassword( String password ) { + this.password = password; + } + + /** + * 返回 H2 数据源所需的初始化SQL脚本列表。 + * + * @return 建立 H2 数据源所需的初始化SQL脚本列表 + */ + public List getInitSqlScripts() { + return initSqlScripts; + } + + /** + * 设置 H2 数据源所需的初始化SQL脚本列表。 + * + * @param initSqlScripts + * 建立 H2 数据源所需的初始化SQL脚本列表 + */ + public void setInitSqlScripts( List initSqlScripts ) { + this.initSqlScripts = initSqlScripts; + } + + /** 将初始化脚本转化为本地文件 */ + private File toLocalFile( Resource sqlScript, File dir ) throws IOException { + if( !sqlScript.exists() ) { + throw new FileNotFoundException( String.format( "初始化脚本文件 '%s' 不存在。", sqlScript.getFilename() ) ); //$NON-NLS-1$ + } + + File scriptFile = new File( dir, new Date().getTime() + ".sql" ); //$NON-NLS-1$ + + InputStream in = sqlScript.getInputStream(); + OutputStream out = new BufferedOutputStream( new FileOutputStream( scriptFile ) ); + + try { + this.copy( in, out ); + return scriptFile; + } finally { + if( in != null ) + in.close(); + if( out != null ) + out.close(); + } + } + + /** 拷贝流。 */ + private long copy( InputStream input, OutputStream output ) throws IOException { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while( -1 != ( n = input.read( buffer ) ) ) { + output.write( buffer, 0, n ); + count += n; + } + return count; + } + + /** 获取初始化脚本文件所在本地目录。 */ + private File getWorkingDir() { + File workingDir = new File( System.getProperty( "java.io.tmpdir" ), "h2_init_sql_scripts" ); //$NON-NLS-1$ //$NON-NLS-2$ + workingDir.mkdir(); + return workingDir; + } +} diff --git a/test_input/ksa/ksa-debug/src/main/resources/log4j.properties b/test_input/ksa/ksa-debug/src/main/resources/log4j.properties new file mode 100644 index 0000000..490da4f --- /dev/null +++ b/test_input/ksa/ksa-debug/src/main/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootCategory=WARN,console + +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout +log4j.appender.console.layout.ConversionPattern=%-5p %d{HH:mm:ss} [%t]%n\t%c:%M %n\t%m%n + +# logging configuration +log4j.category.com.ksa=DEBUG +log4j.category.java.sql=DEBUG +log4j.category.org.apache.shiro=DEBUG \ No newline at end of file diff --git a/test_input/ksa/ksa-debug/src/main/resources/struts.properties b/test_input/ksa/ksa-debug/src/main/resources/struts.properties new file mode 100644 index 0000000..73c9757 --- /dev/null +++ b/test_input/ksa/ksa-debug/src/main/resources/struts.properties @@ -0,0 +1,2 @@ +### when set to true, Struts will act much more friendly for developers. +struts.devMode=true \ No newline at end of file diff --git a/test_input/ksa/ksa-debug/src/main/resources/test/mybatis-test-context.xml b/test_input/ksa/ksa-debug/src/main/resources/test/mybatis-test-context.xml new file mode 100644 index 0000000..4c4a0ca --- /dev/null +++ b/test_input/ksa/ksa-debug/src/main/resources/test/mybatis-test-context.xml @@ -0,0 +1,21 @@ + + + + + + + + + + init.sql + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-service-root/ksa-bd-service/pom.xml b/test_input/ksa/ksa-service-root/ksa-bd-service/pom.xml new file mode 100644 index 0000000..3e8394d --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-bd-service/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + + com.ksa + ksa-service-root + 3.9.0 + + + ksa-bd-service + jar + + ksa-bd-service + 杭州凯思爱物流管理系统 - 基础数据管理 Service 模块 + + + + com.ksa + ksa-bd-dao + ${project.version} + + + diff --git a/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/BasicDataServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/BasicDataServiceImpl.java new file mode 100644 index 0000000..e9737a5 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/BasicDataServiceImpl.java @@ -0,0 +1,69 @@ +package com.ksa.service.bd.impl; + +import com.ksa.dao.bd.BasicDataDao; +import com.ksa.model.ModelUtils; +import com.ksa.model.bd.BasicData; +import com.ksa.service.bd.BasicDataService; + + +public class BasicDataServiceImpl implements BasicDataService { + + private BasicDataDao basicDataDao; + + @Override + public BasicData loadBasicDataById( String dataId ) throws RuntimeException { + BasicData data = basicDataDao.selectBasicDataById( dataId ); + if( data == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的基本代码不存在。", dataId ) ); + } + return data; + } + + @Override + public BasicData createBasicData( BasicData data ) throws RuntimeException { + // 设置 id + data.setId( ModelUtils.generateRandomId() ); + + int length = basicDataDao.insertBasicData( data ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "新增基本代码发生异常,期望新增 1 条数据,实际新增了 %d 条数据。", length ) ); + } + + return data; + } + + @Override + public BasicData modifyBasicData( BasicData data ) throws RuntimeException { + BasicData temp = basicDataDao.selectBasicDataById( data.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的基本代码不存在。", data.getId() ) ); + } + int length = basicDataDao.updateBasicData( data ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新基本代码发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", length ) ); + } + return data; + } + + @Override + public BasicData removeBasicData( BasicData data ) throws RuntimeException { + BasicData temp = basicDataDao.selectBasicDataById( data.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的基本代码不存在。", data.getId() ) ); + } + + if( ModelUtils.isReservedObject( temp ) ) { + throw new IllegalArgumentException( String.format( "名称为 '%s' 的基本代码是系统保留数据,无法删除。", temp.getName() ) ); + } + + int length = basicDataDao.deleteBasicData( temp ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除基本代码发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + return temp; + } + + public void setBasicDataDao( BasicDataDao basicDataDao ) { + this.basicDataDao = basicDataDao; + } +} diff --git a/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/CurrencyRateServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/CurrencyRateServiceImpl.java new file mode 100644 index 0000000..73c3a70 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/CurrencyRateServiceImpl.java @@ -0,0 +1,207 @@ +package com.ksa.service.bd.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.StringUtils; + +import com.ksa.dao.bd.CurrencyRateDao; +import com.ksa.model.ModelUtils; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.bd.Partner; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.util.Assert; + + +public class CurrencyRateServiceImpl implements CurrencyRateService { + + private CurrencyRateDao currencyRateDao; + + public void setCurrencyRateDao( CurrencyRateDao currencyRateDao ) { + this.currencyRateDao = currencyRateDao; + } + + @Override + public CurrencyRate saveCurrencyRate( CurrencyRate rate ) throws RuntimeException { + int length = 0; + if( StringUtils.hasText( rate.getId() ) ) { + CurrencyRate temp = currencyRateDao.selectRateById( rate.getId() ); + if( temp != null && temp.getMonth() != null && temp.getMonth().equals( rate.getMonth() ) ) { + length = currencyRateDao.updateRate( rate ); + } else { + rate.setId( ModelUtils.generateRandomId() ); + length = currencyRateDao.insertRate( rate ); + } + } else { + rate.setId( ModelUtils.generateRandomId() ); + length = currencyRateDao.insertRate( rate ); + } + + if( length != 1 ) { + throw new IllegalStateException( String.format( "保存货币汇率发生异常,期望保存 1 条数据,实际保存了 %d 条数据。", length ) ); + } + return rate; + } + + @Override + public List loadLatestCurrencyRates() throws RuntimeException { + return loadLatestCurrencyRates( new Date() ); + } + + @Override + public List loadLatestCurrencyRates( Date date ) throws RuntimeException { + // 获取系统已有的货币清单,为清单中的货币查询最新汇率 + List currencies = currencyRateDao.selectAllCurrency(); + if( currencies == null || currencies.size() <= 0 ) { + return Collections.emptyList(); + } + List rates = currencyRateDao.selectLatestRates( date ); + + // 按货币标识分组 + Map map = new HashMap(); + if( rates != null && rates.size() > 0 ) { + for( CurrencyRate rate : rates ) { + map.put( rate.getCurrency().getId(), rate ); + } + } + + List result = new ArrayList( currencies.size() ); + for( Currency c : currencies ) { + if( map.containsKey( c.getId() ) ) { + result.add( map.get( c.getId() ) ); + } else { + // 没有对货币设定任何汇率值,沿用默认值 + CurrencyRate rate = new CurrencyRate(); + rate.setCurrency( c ); + rate.setRate( c.getDefaultRate() ); + rate.setMonth( new Date() ); + result.add( rate ); + } + } + return result; + } + + @Override + public CurrencyRate loadLatestCurrencyRate( String currencyId ) throws RuntimeException { + return loadLatestCurrencyRate( currencyId, new Date() ); + } + + @Override + public CurrencyRate loadLatestCurrencyRate( String currencyId, Date date ) throws RuntimeException { + CurrencyRate rate = currencyRateDao.selectLatestRate( currencyId, date ); + if( rate == null ) { + Currency currency = currencyRateDao.selectCurrencyById( currencyId ); + Assert.notNull( currency, + String.format( "标识为 'currencyId' 的结算货币不存在!请先添加相应的货币,再获取其汇率值。", currencyId ) ); + rate = new CurrencyRate(); + rate.setCurrency( currency ); + rate.setRate( currency.getDefaultRate() ); + rate.setMonth( new Date() ); + } + return rate; + } + + @Override + public List loadPartnerCurrencyRates( String customerId ) throws RuntimeException { + // 获取系统已有的货币清单,为清单中的货币查询相应的汇率 + List currencies = currencyRateDao.selectAllCurrency(); + if( currencies == null || currencies.size() <= 0 ) { + return Collections.emptyList(); + } + + List partnerRates = currencyRateDao.selectRateByPartner( customerId ); + + // 按货币标识分组 + Map partnerRateMap = new HashMap(); + if( partnerRates != null && partnerRates.size() > 0 ) { + for( CurrencyRate rate : partnerRates ) { + partnerRateMap.put( rate.getCurrency().getId(), rate ); + } + } + + List latestRates = currencyRateDao.selectLatestRates( new Date() ); + + // 按货币标识分组 + Map latestRateMap = new HashMap(); + if( latestRates != null && latestRates.size() > 0 ) { + for( CurrencyRate rate : latestRates ) { + latestRateMap.put( rate.getCurrency().getId(), rate ); + } + } + + Partner customer = new Partner(); + customer.setId( customerId ); + + List result = new ArrayList( currencies.size() ); + for( Currency c : currencies ) { + if( partnerRateMap.containsKey( c.getId() ) ) { + result.add( partnerRateMap.get( c.getId() ) ); + } else if( latestRateMap.containsKey( c.getId() ) ) { + CurrencyRate latestRate = latestRateMap.get( c.getId() ); + latestRate.setId( "" ); + latestRate.setPartner( customer ); + result.add( latestRate ); + } else { + // 没有对货币设定任何汇率值,沿用默认值 + CurrencyRate rate = new CurrencyRate(); + rate.setCurrency( c ); + rate.setRate( c.getDefaultRate() ); + rate.setPartner( customer ); + result.add( rate ); + } + } + return result; + } + + @Override + public CurrencyRate loadPartnerCurrencyRate( String customerId, String currencyId ) throws RuntimeException { + CurrencyRate rate = currencyRateDao.selectRateByPartner( customerId, currencyId ); + if( rate == null ) { + Currency currency = currencyRateDao.selectCurrencyById( currencyId ); + Assert.notNull( currency, + String.format( "标识为 'currencyId' 的结算货币不存在!请先添加相应的货币,再获取其汇率值。", currencyId ) ); + rate = new CurrencyRate(); + rate.setCurrency( currency ); + rate.setRate( currency.getDefaultRate() ); + rate.setPartner( new Partner() ); + rate.getPartner().setId( customerId ); + } + return rate; + } + + @Override + public List loadAllCurrencyRates() throws RuntimeException { + // 获取系统已有的货币清单,为清单中的货币查询最新汇率 + List currencies = currencyRateDao.selectAllCurrency(); + if( currencies == null || currencies.size() <= 0 ) { + return Collections.emptyList(); + } + List rates = currencyRateDao.selectAllRates(); + + // 按货币标识分组 + Map map = new HashMap(); + if( rates != null && rates.size() > 0 ) { + for( CurrencyRate rate : rates ) { + map.put( rate.getCurrency().getId(), rate ); + } + } + + List result = new ArrayList( rates ); + for( Currency c : currencies ) { + if( ! map.containsKey( c.getId() ) ) { + // 没有对货币设定任何汇率值,沿用默认值 + CurrencyRate rate = new CurrencyRate(); + rate.setCurrency( c ); + rate.setRate( c.getDefaultRate() ); + rate.setMonth( new Date() ); + result.add( rate ); + } + } + return result; + } +} diff --git a/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/PartnerServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/PartnerServiceImpl.java new file mode 100644 index 0000000..d331cb3 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/impl/PartnerServiceImpl.java @@ -0,0 +1,155 @@ +package com.ksa.service.bd.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.ksa.dao.bd.PartnerDao; +import com.ksa.model.ModelUtils; +import com.ksa.model.bd.Partner; +import com.ksa.model.bd.PartnerType; +import com.ksa.service.bd.PartnerService; + + +public class PartnerServiceImpl implements PartnerService { + + private PartnerDao partnerDao; + + @Override + public Partner loadPartnerById( String id ) throws RuntimeException { + Partner partner = partnerDao.selectPartnerById( id ); + if( partner == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的合作伙伴不存在。", id ) ); + } + return partner; + } + + @Override + public Partner loadPartnerByCode( String code ) throws RuntimeException { + Partner partner = partnerDao.selectPartnerByCode( code ); + if( partner == null ) { + throw new IllegalArgumentException( String.format( "代码为 '%s' 的合作伙伴不存在。", code ) ); + } + return partner; + } + + @Override + public Partner createPartner( Partner partner ) throws RuntimeException { + // 保证合作伙伴的代码全局唯一 + Partner temp = partnerDao.selectPartnerByCode( partner.getCode() ); + if( temp != null ) { + throw new IllegalStateException( String.format( "代码为 '%s' 的合作伙伴已经存在。", partner.getCode() ) ); + } + + // 设置 id + partner.setId( ModelUtils.generateRandomId() ); + + int length = partnerDao.insertPartner( partner ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "新增合作伙伴发生异常,期望新增 1 条数据,实际新增了 %d 条数据。", length ) ); + } + + // 插入类型 + Collection types = buildMap( partner.getTypes() ).values(); // 防止重复数据被插入 + for( PartnerType type : types ) { + partnerDao.insertPartnerType( partner, type ); + } + + // 插入附加提单信息 + Set extras = buildSet( partner.getExtras() ); + for( String extra : extras ) { + partnerDao.insertPartnerExtra( partner, extra ); + } + + return partner; + } + + @Override + public Partner modifyPartner( Partner partner ) throws RuntimeException { + Partner temp = partnerDao.selectPartnerById( partner.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的合作伙伴不存在。", partner.getId() ) ); + } else if( !temp.getCode().equals( partner.getCode() ) ) { // 修改了 code + temp = partnerDao.selectPartnerByCode( partner.getCode() ); + if( temp != null ) { + throw new IllegalStateException( String.format( "代码为 '%s' 的合作伙伴已经存在。", partner.getCode() ) ); + } + } + + int length = partnerDao.updatePartner( partner ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新合作伙伴发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", length ) ); + } + + // 更新类型信息 + Map oldTypes = buildMap( temp.getTypes() ); + Map newTypes = buildMap( partner.getTypes() ); + for( PartnerType type : oldTypes.values() ) { + if( !newTypes.containsKey( type.getId() ) ) { + partnerDao.deletePartnerType( partner, type ); + } + } + for( PartnerType type : newTypes.values() ) { + if( !oldTypes.containsKey( type.getId() ) ) { + partnerDao.insertPartnerType( partner, type ); + } + } + + // 更新附加提单信息 + Set oldExtras = buildSet( temp.getExtras() ); + Set newExtras = buildSet( partner.getExtras() ); + for( String extra : oldExtras ) { + if( !newExtras.contains( extra ) ) { + partnerDao.deletePartnerExtra( partner, extra ); + } + } + for( String extra : newExtras ) { + if( !oldExtras.contains( extra ) ) { + partnerDao.insertPartnerExtra( partner, extra ); + } + } + + return partner; + } + + private Map buildMap( PartnerType[] types ) { + Map map = new HashMap(); + if( types != null && types.length > 0 ) { + for( PartnerType type : types ) { + map.put( type.getId(), type ); + } + } + return map; + } + + private Set buildSet( String[] extras ) { + Set set = new HashSet(); + if( extras != null && extras.length > 0 ) { + for( String extra : extras ) { + set.add( extra ); + } + } + return set; + } + + @Override + public Partner removePartner( Partner partner ) throws RuntimeException { + Partner temp = partnerDao.selectPartnerById( partner.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的合作伙伴不存在。", partner.getId() ) ); + } + + int length = partnerDao.deletePartner( temp ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除合作伙伴发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + return temp; + } + + public void setPartnerDao( PartnerDao partnerDao ) { + this.partnerDao = partnerDao; + } + +} diff --git a/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/util/BasicDataUtils.java b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/util/BasicDataUtils.java new file mode 100644 index 0000000..52d2f03 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/java/com/ksa/service/bd/util/BasicDataUtils.java @@ -0,0 +1,131 @@ +package com.ksa.service.bd.util; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.ksa.context.ContextException; +import com.ksa.context.ServiceContextUtils; +import com.ksa.dao.bd.BasicDataDao; +import com.ksa.model.bd.BasicData; + + +public class BasicDataUtils { + + private static BasicDataDao dao; + private static boolean initialized = false; + + // 保存所有的基础数据缓存 + private static Map allBasicDataCache = new HashMap(); + // 按照基础数据类型,分类保存基础数据 key: typeId + private static Map> typedBasicDataCache = + new HashMap>(); + + static { + try { + BasicDataDao d = ServiceContextUtils.getService( BasicDataDao.class ); + init( d ); + } catch( ContextException e ) { + // 忽略 + } + } + + public static void init( BasicDataDao basicDataDao ) { + if( ! initialized && basicDataDao != null ) { + dao = basicDataDao; + List allData = dao.selectAllBasicData(); + for( BasicData data : allData ) { + addToCache( data ); + } + initialized = true; + } + } + + public static BasicData getDataFromName( String name ) { + return getDataFromName( name, allBasicDataCache.values() ); + } + + public static BasicData getDataFromName( String name, String... typeIds ) { + for( String typeId : typeIds ) { + if( typedBasicDataCache.containsKey( typeId ) ) { + BasicData data = getDataFromName( name, typedBasicDataCache.get( typeId ).values() ); + if( data != null ) { + return data; + } + } + } + return null; + } + + private static BasicData getDataFromName( String name, Collection data ) { + for( BasicData d : data ) { + if( d.getName().equals( name ) ) { + return d; + } + } + return null; + } + + public static BasicData getData( String id ) { + if( allBasicDataCache.containsKey( id ) ) { + return allBasicDataCache.get( id ); + } else { + BasicData d = dao.selectBasicDataById( id ); + if( d != null ) { + addToCache( d ); + return d; + } else { + return new BasicData(); + } + } + } + + public static void updateData( String id ) { + BasicData d = dao.selectBasicDataById( id ); + if( d != null ) { + // 更新 + addToCache( d ); + } else { + // 删除 + BasicData data = allBasicDataCache.remove( id ); + if( data != null ) { + String typeId = data.getType().getId(); + if( typedBasicDataCache.containsKey( typeId ) ) { + typedBasicDataCache.get( typeId ).remove( id ); + } + } + } + } + + public static void clearCache() { + clearCache( null ); + } + + public static void clearCache( String typeId ) { + if( typeId != null ) { + Map typedData = typedBasicDataCache.remove( typeId ); + for( String key : typedData.keySet() ) { + allBasicDataCache.remove( key ); + } + } else { + typedBasicDataCache.clear(); + allBasicDataCache.clear(); + } + } + + private static void addToCache( BasicData data ) { + String id = data.getId(); + allBasicDataCache.put( id, data ); + String typeId = data.getType().getId(); + if( typedBasicDataCache.containsKey( typeId ) ) { + typedBasicDataCache.get( typeId ).put( id, data ); + } else { + Map map = new HashMap(); + map.put( id, data ); + typedBasicDataCache.put( typeId, map ); + } + } + + +} diff --git a/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/resources/spring/service/bd-service-context.xml b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/resources/spring/service/bd-service-context.xml new file mode 100644 index 0000000..6004f80 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-bd-service/src/main/resources/spring/service/bd-service-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-service-root/ksa-finance-service/pom.xml b/test_input/ksa/ksa-service-root/ksa-finance-service/pom.xml new file mode 100644 index 0000000..ce03c61 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-finance-service/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + + com.ksa + ksa-service-root + 3.9.0 + + + ksa-finance-service + jar + + ksa-finance-service + 杭州凯思爱物流管理系统 - 财务管理 Service 模块 + + + UTF-8 + + + + + com.ksa + ksa-finance-dao + 3.9.0 + + + diff --git a/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/AccountServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/AccountServiceImpl.java new file mode 100644 index 0000000..2012539 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/AccountServiceImpl.java @@ -0,0 +1,249 @@ +package com.ksa.service.finance.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.util.StringUtils; + +import com.ksa.dao.finance.AccountCurrencyRateDao; +import com.ksa.dao.finance.AccountDao; +import com.ksa.model.ModelUtils; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.AccountCurrencyRate; +import com.ksa.model.finance.AccountState; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.service.finance.AccountService; + + +public class AccountServiceImpl implements AccountService { + + private AccountDao accountDao; + private AccountCurrencyRateDao accountCurrencyRateDao; + + @Override + public int querySimilarAccountCodeCount( String partnerCode ) throws RuntimeException { + return accountDao.querySimilarAccountCodeCount( partnerCode ); + } + + @Override + public Account loadAccountById( String accountId ) throws RuntimeException { + Account account = accountDao.selectAccountById( accountId ); + if( account == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的结算/对账单不存在。", accountId ) ); + } + return account; + } + + @Override + public Account removeInvoice( Account account ) throws RuntimeException { + Account temp = accountDao.selectAccountById( account.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的结算/对账单不存在。", account.getId() ) ); + } + + // 当发票拿去销账的对应结算单已经确认结算完毕后,不能删除 + if( !AccountState.isNone( temp.getState() ) ) { + throw new IllegalStateException( String.format( "结算单已审核通过,无法删除。", temp.getCode() ) ); + } + + // 删除 + int length = accountDao.deleteAccount( temp ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除结算单发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + + return temp; + } + + @Override + public Account saveAccount( Account account, List rates ) throws RuntimeException { + if( StringUtils.hasText( account.getId() ) ) { + Account temp = accountDao.selectAccountById( account.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的结算/对账单不存在。", account.getId() ) ); + } + if( AccountState.isNone( temp.getState() ) || AccountState.isProcessing( temp.getState() ) ) { + // 审核前的结算单,还未进入流程前 可以任意修改 + temp.setCreatedDate( account.getCreatedDate() ); + temp.setDeadline( account.getDeadline() ); + temp.setPaymentDate( account.getPaymentDate() ); + temp.setNote( account.getNote() ); + temp.setCode( account.getCode() ); + temp.setTarget( account.getTarget() ); + accountDao.updateAccount( temp ); + + // 更新费用清单 + List oldCharges = temp.getCharges(); + Set oldChargeSet = generateChargeIdSet( oldCharges ); + List newCharges = account.getCharges(); + Set newChargeSet = generateChargeIdSet( newCharges ); + if( newCharges != null && newCharges.size() > 0 ) { + List needInsertedCharges = new ArrayList(); + for( Charge newCharge : newCharges ) { + if( ! oldChargeSet.contains( newCharge.getId() ) ) { + // 新增的费用 + needInsertedCharges.add( newCharge ); + } + } + if( needInsertedCharges.size() > 0 ) { + accountDao.insertAccountCharges( temp, needInsertedCharges ); + } + } + + if( oldCharges != null && oldCharges.size() > 0 ) { + List needDeletedCharges = new ArrayList(); + for( Charge oldCharge : oldCharges ) { + if( ! newChargeSet.contains( oldCharge.getId() ) ) { + // 删除的费用 + needDeletedCharges.add( oldCharge ); + } + } + if( needDeletedCharges.size() > 0 ) { + accountDao.deleteAccountCharges( temp, needDeletedCharges ); + } + } + + // 更新汇率 + if( rates != null && rates.size() > 0 ) { // 保存汇率 + for( AccountCurrencyRate rate : rates ) { + rate.setAccount( temp ); + if( ! StringUtils.hasText( rate.getId() ) ) { + rate.setId( ModelUtils.generateRandomId() ); + accountCurrencyRateDao.insertRate( rate ); + } else { + accountCurrencyRateDao.updateRate( rate ); + } + } + } + } else { + // 已进入审核流程的结算单 只能修改有限的数据 + // TODO 具体可以修改什么 暂时待定 + temp.setDeadline( account.getDeadline() ); + temp.setPaymentDate( account.getPaymentDate() ); + temp.setNote( account.getNote() ); + accountDao.updateAccount( temp ); + } + } else { + // 新建 + account.setId( ModelUtils.generateRandomId() ); + accountDao.insertAccount( account ); // 保存结算单 + List charges = account.getCharges(); + if( charges != null && charges.size() > 0 ) { + accountDao.insertAccountCharges( account, charges ); // 保存结算单中的费用清单 + } + if( rates != null && rates.size() > 0 ) { // 保存汇率 + for( AccountCurrencyRate rate : rates ) { + rate.setAccount( account ); + rate.setId( ModelUtils.generateRandomId() ); + accountCurrencyRateDao.insertRate( rate ); + } + } + } + return accountDao.selectAccountById( account.getId() ); + } + + private Set generateChargeIdSet( List charges ) { + Set set = new HashSet(); + if( charges != null && charges.size() > 0 ) { + for( Charge charge : charges ) { + set.add( charge.getId() ); + } + } + return set; + } + + @Override + public Account updateAccountState( Account account ) throws RuntimeException { + Account temp = accountDao.selectAccountById( account.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的结算单不存在。", account.getId() ) ); + } + // FIXME 需要判断状态改变的合理性 + temp.setState( account.getState() ); + accountDao.updateAccountState( account ); + return temp; + } + + @Override + public List loadAccountBookingNotes( Account account ) throws RuntimeException { + return accountDao.selectBookingNoteByAccountId( account.getId() ); + } + + @Override + public List loadAccountCurrencyRates( Account account ) throws RuntimeException { + if( account == null ) { + return accountCurrencyRateDao.selectLatestRates(); + } + + // 获取最新汇率 + List latestRates = accountCurrencyRateDao.selectLatestRates(); + Map latestRateMap = generateRateMap( latestRates ); + + // 获取系统已有的货币清单,为清单中的货币查询相应的汇率 + List currencies = accountCurrencyRateDao.selectAllCurrency(); + if( currencies == null || currencies.size() <= 0 ) { + return Collections.emptyList(); + } + + // 获取结算单对应的汇率 + List rates = accountCurrencyRateDao.selectRatesByAccountId( account.getId() ); + Map rateMap = generateRateMap( rates ); + + // 获取客户对应的汇率 + Map partnerRateMap = new HashMap(); + if( account.getTarget() != null && StringUtils.hasText( account.getTarget().getId() ) ) { + List partnerRates = accountCurrencyRateDao.selectRateByPartner( account.getTarget().getId() ); + partnerRateMap = generateRateMap( partnerRates ); + } + + List result = new ArrayList( currencies.size() ); + for( Currency c : currencies ) { + String currencyId = c.getId(); + if( rateMap.containsKey( currencyId ) ) { + result.add( rateMap.get( currencyId ) ); + } else { + AccountCurrencyRate rate = new AccountCurrencyRate(); + rate.setAccount( account ); + rate.setCurrency( c ); + if( partnerRateMap.containsKey( currencyId ) ) { + rate.setRate( partnerRateMap.get( currencyId ).getRate() ); + } else if( latestRateMap.containsKey( currencyId ) ) { + rate.setRate( latestRateMap.get( currencyId ).getRate() ); + } else { + rate.setRate( c.getDefaultRate() ); + } + result.add( rate ); + } + } + return result; + } + + private Map generateRateMap( List rates ) { + Map rateMap = new HashMap(); + if( rates != null && rates.size() > 0 ) { + for( CurrencyRate rate : rates ) { + rateMap.put( rate.getCurrency().getId(), rate ); + } + } + return rateMap; + } + + public void setAccountDao( AccountDao accountDao ) { + this.accountDao = accountDao; + } + + public void setAccountCurrencyRateDao( AccountCurrencyRateDao accountCurrencyRateDao ) { + this.accountCurrencyRateDao = accountCurrencyRateDao; + } + + + +} diff --git a/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/ChargeServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/ChargeServiceImpl.java new file mode 100644 index 0000000..e1545e4 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/ChargeServiceImpl.java @@ -0,0 +1,448 @@ +package com.ksa.service.finance.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.StringUtils; + +import com.ksa.dao.finance.ChargeDao; +import com.ksa.dao.logistics.BookingNoteDao; +import com.ksa.model.ModelUtils; +import com.ksa.model.finance.BookingNoteChargeState; +import com.ksa.model.finance.Charge; +import com.ksa.model.finance.FinanceModel; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.logistics.BookingNoteState; +import com.ksa.service.finance.ChargeService; + + +public class ChargeServiceImpl implements ChargeService { + + private ChargeDao chargeDao; + private BookingNoteDao bookingNoteDao; // 改变托单状态 + + @Override + public List loadBookingNoteCharges( String bookingNoteId ) throws RuntimeException { + return chargeDao.selectChargeByBookingNoteId( bookingNoteId ); + } + + @Override + public List loadBookingNoteCharges( String bookingNoteId, int direction, int nature ) throws RuntimeException { + return chargeDao.selectChargeByBookingNoteId( bookingNoteId, direction, nature ); + } + + @Override + public List loadBookingNoteCharges( String bookingNoteId, List incomes, List expenses ) throws RuntimeException { + List charges = chargeDao.selectChargeByBookingNoteId( bookingNoteId ); + if( charges == null || charges.isEmpty() ) { + return Collections.emptyList(); + } + + if( incomes != null ) { + for( Charge charge : charges ) { + if( Charge.isIncome( charge ) ) { + incomes.add( charge ); + } + } + // 已通过SQL进行排序 + //Collections.sort( incomes ); + } + + if( expenses != null ) { + for( Charge charge : charges ) { + if( Charge.isExpense( charge ) ) { + expenses.add( charge ); + } + } + // 已通过SQL进行排序 + //Collections.sort( expenses ); + } + return charges; + } + + @Override + public List loadBookingNoteCharges( String bookingNoteId, List incomes, List expenses, int nature ) throws RuntimeException { + + List charges = chargeDao.selectChargeByBookingNoteId( bookingNoteId ); + if( charges == null || charges.isEmpty() ) { + return Collections.emptyList(); + } + + List filteredCharges = new ArrayList( charges.size() ); + for( Charge charge : charges ) { + if( charge.getNature() == nature ) { + filteredCharges.add( charge ); + } + } + + if( incomes != null ) { + for( Charge charge : filteredCharges ) { + if( Charge.isIncome( charge ) ) { + incomes.add( charge ); + } + } + Collections.sort( incomes ); + } + + if( expenses != null ) { + for( Charge charge : filteredCharges ) { + if( Charge.isExpense( charge ) ) { + expenses.add( charge ); + } + } + Collections.sort( expenses ); + } + return filteredCharges; + } + + @Override + public BookingNote updateBookingNoteChargeState( BookingNote note ) { + BookingNote temp = bookingNoteDao.selectBookingNoteById( note.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的托单不存在。", note.getId() ) ); + } + int oldState = temp.getState(); + int newState = note.getState(); + // 15 = 0x0000000F + // 将 对应费用的 4位状态位置0, 然后再赋予新值 + int tempState = ( oldState & ~0xF ); + newState = tempState | newState; + + // FIXME 需要判断状态改变的合理性 + temp.setState( newState ); + note.setState( newState ); + bookingNoteDao.updateBookingNoteState( temp ); + return temp; + } + + @Override + public BookingNote updateBookingNoteChargeState( BookingNote note, int nature ) { + BookingNote temp = bookingNoteDao.selectBookingNoteById( note.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的托单不存在。", note.getId() ) ); + } + int oldState = temp.getState(); + int newState = note.getState(); + // 15 = 0x0000000F + // 将 对应费用的 4位状态位置0, 然后再赋予新值 + int shift = BookingNoteChargeState.computeShift( nature ); + int tempState = ( oldState & ~( 0xF << shift ) ); + newState = tempState | ( newState << shift ); + + // FIXME 需要判断状态改变的合理性 + temp.setState( newState ); + note.setState( newState ); + bookingNoteDao.updateBookingNoteState( temp ); + return temp; + } + + @Override + public BookingNote updateBookingNoteChargeState( BookingNote note, int direction, int nature ) { + BookingNote temp = bookingNoteDao.selectBookingNoteById( note.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的托单不存在。", note.getId() ) ); + } + int oldState = temp.getState(); + int newState = note.getState(); + // 15 = 0x0000000F + // 将 对应费用的 4位状态位置0, 然后再赋予新值 + int shift = BookingNoteChargeState.computeShift( direction, nature ); + int tempState = ( oldState & ~( 0xF << shift ) ); + newState = tempState | ( newState << shift ); + + // FIXME 需要判断状态改变的合理性 + temp.setState( newState ); + note.setState( newState ); + bookingNoteDao.updateBookingNoteState( temp ); + return temp; + } + + @Override + public List saveBookingNoteCharges( BookingNote note, List charges, int direction, int nature ) throws RuntimeException { + if( BookingNoteState.isChecking( note.getState() ) ) { + throw new IllegalStateException( "托单费用信息已经提交审核,暂时不能进行更改。" ); + } + // 获取托单的原有费用列表 + List oldCharges = chargeDao.selectChargeByBookingNoteId( note.getId(), direction, nature ); + Map oldChargeMap = getChargeMap( oldCharges ); + + // 托单的新费用列表 + List newCharges = new ArrayList(); + if( charges != null && charges.size() > 0 ) { + setRank( charges ); + newCharges.addAll( charges ); + } + Map newChargeMap = getChargeMap( newCharges ); // 过滤掉了新增费用( 没有 id 属性的费用 ) + + // 首先处理新增的费用 + for( Charge charge : newCharges ) { + if( charge == null ) { + continue; + } + if( ! StringUtils.hasText( charge.getId() ) ) { + charge.setId( ModelUtils.generateRandomId() ); + charge.getBookingNote().setId( note.getId() ); + int length = chargeDao.insertCharge( charge ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "新增单条费用数据发生异常,期望新增 1 条数据,实际新增了 %d 条数据。", length ) ); + } + } + } + + // 接着处理删除的费用: 新列表中没有, 但旧列表中有的数据 + // 和 更新费用 + for( String oldChargeId : oldChargeMap.keySet() ) { + if( ! newChargeMap.containsKey( oldChargeId ) ) { + // 新列表中不存在则需要删除 + int length = chargeDao.deleteCharge( oldChargeMap.get( oldChargeId ) ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除单条费用数据发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + } else { + // 新列表中存在, 需要更新则更新 + Charge newCharge = newChargeMap.get( oldChargeId ); + if( needUpdate( oldChargeMap.get( oldChargeId ), newCharge ) ) { + newCharge.getBookingNote().setId( note.getId() ); + int length = chargeDao.updateCharge( newCharge ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新单条费用数据发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", length ) ); + } + } + } + } + + // 保存完毕, 读取最新的数据并返回 + charges.clear(); + newCharges = loadBookingNoteCharges( note.getId(), direction, nature ); + + if( oldCharges.size() == 0 && newCharges.size() > 0 ) { + // 状态改为录入中 + note.setState( BookingNoteChargeState.ENTERING ); + this.updateBookingNoteChargeState( note, direction, nature ); + } else if( oldCharges.size() > 0 && newCharges.size() == 0 ) { + // 状态改为还未录入 + note.setState( BookingNoteChargeState.NONE ); + this.updateBookingNoteChargeState( note, direction, nature ); + } + + // 更新记账月份 + bookingNoteDao.updateBookingNoteChargeDate( note ); + + return newCharges; + } + + /*@Override + public List saveBookingNoteCharges( BookingNote note, List incomes, List expenses ) throws RuntimeException { + return saveBookingNoteCharges( note, incomes, expenses, 0 ); + } */ + + // 设置录入顺序,解决排序问题 + private void setRank(List charges) { + int rank = 1; + for(Charge c : charges) { + c.setRank(rank++); + } + } + + @Override + public List saveBookingNoteCharges( BookingNote note, List incomes, List expenses ) throws RuntimeException { + if( BookingNoteChargeState.isChecking( note.getState() ) ) { + throw new IllegalStateException( "托单费用信息已经提交审核,暂时不能进行更改。" ); + } + // 获取托单的原有费用列表 + List oldCharges = loadBookingNoteCharges( note.getId(), null, null ); + Map oldChargeMap = getChargeMap( oldCharges ); + + // 托单的新费用列表 + List newCharges = new ArrayList(); + if( incomes != null && incomes.size() > 0 ) { + setRank( incomes ); + newCharges.addAll( incomes ); + } + if( expenses != null && expenses.size() > 0 ) { + setRank( expenses ); + newCharges.addAll( expenses ); + } + Map newChargeMap = getChargeMap( newCharges ); // 过滤掉了新增费用( 没有 id 属性的费用 ) + + // 首先处理新增的费用 + for( Charge charge : newCharges ) { + if( charge == null ) { + continue; + } + if( ! StringUtils.hasText( charge.getId() ) ) { + charge.setId( ModelUtils.generateRandomId() ); + charge.getBookingNote().setId( note.getId() ); + int length = chargeDao.insertCharge( charge ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "新增单条费用数据发生异常,期望新增 1 条数据,实际新增了 %d 条数据。", length ) ); + } + } + } + + // 接着处理删除的费用: 新列表中没有, 但旧列表中有的数据 + // 和 更新费用 + for( String oldChargeId : oldChargeMap.keySet() ) { + if( ! newChargeMap.containsKey( oldChargeId ) ) { + // 新列表中不存在则需要删除 + int length = chargeDao.deleteCharge( oldChargeMap.get( oldChargeId ) ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除单条费用数据发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + } else { + // 新列表中存在, 需要更新则更新 + Charge newCharge = newChargeMap.get( oldChargeId ); + if( needUpdate( oldChargeMap.get( oldChargeId ), newCharge ) ) { + newCharge.getBookingNote().setId( note.getId() ); + int length = chargeDao.updateCharge( newCharge ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新单条费用数据发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", length ) ); + } + } + } + } + + // 保存完毕, 读取最新的数据并返回 + incomes.clear(); + expenses.clear(); + newCharges = loadBookingNoteCharges( note.getId(), incomes, expenses ); + + if( newCharges.size() > 0 ) { + // 状态改为录入中 + note.setState( BookingNoteChargeState.setEntering( note.getState() ) ); + bookingNoteDao.updateBookingNoteState( note ); + } else if( oldCharges.size() > 0 && newCharges.size() == 0 ) { + // 状态改为还未录入 + note.setState( BookingNoteChargeState.setUnentering( note.getState() ) ); + bookingNoteDao.updateBookingNoteState( note ); + } + + // 更新记账月份 + bookingNoteDao.updateBookingNoteChargeDate( note ); + + return newCharges; + } + + @Override + @Deprecated + public List saveBookingNoteCharges( BookingNote note, List incomes, List expenses, int nature ) throws RuntimeException { + if( BookingNoteChargeState.isChecking( note.getState(), nature ) ) { + throw new IllegalStateException( "托单费用信息已经提交审核,暂时不能进行更改。" ); + } + // 获取托单的原有费用列表 + List oldCharges = loadBookingNoteCharges( note.getId(), null, null ); + Map oldChargeMap = getChargeMap( oldCharges ); + + // 托单的新费用列表 + List newCharges = new ArrayList(); + if( incomes != null && incomes.size() > 0 ) { + setRank( incomes ); + newCharges.addAll( incomes ); + } + if( expenses != null && expenses.size() > 0 ) { + setRank( expenses ); + newCharges.addAll( expenses ); + } + Map newChargeMap = getChargeMap( newCharges ); // 过滤掉了新增费用( 没有 id 属性的费用 ) + + // 首先处理新增的费用 + for( Charge charge : newCharges ) { + if( charge == null ) { + continue; + } + if( ! StringUtils.hasText( charge.getId() ) ) { + charge.setId( ModelUtils.generateRandomId() ); + charge.getBookingNote().setId( note.getId() ); + int length = chargeDao.insertCharge( charge ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "新增单条费用数据发生异常,期望新增 1 条数据,实际新增了 %d 条数据。", length ) ); + } + } + } + + // 接着处理删除的费用: 新列表中没有, 但旧列表中有的数据 + // 和 更新费用 + for( String oldChargeId : oldChargeMap.keySet() ) { + if( ! newChargeMap.containsKey( oldChargeId ) ) { + // 新列表中不存在则需要删除 + int length = chargeDao.deleteCharge( oldChargeMap.get( oldChargeId ) ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除单条费用数据发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + } else { + // 新列表中存在, 需要更新则更新 + Charge newCharge = newChargeMap.get( oldChargeId ); + if( needUpdate( oldChargeMap.get( oldChargeId ), newCharge ) ) { + newCharge.getBookingNote().setId( note.getId() ); + int length = chargeDao.updateCharge( newCharge ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新单条费用数据发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", length ) ); + } + } + } + } + + // 保存完毕, 读取最新的数据并返回 + incomes.clear(); + expenses.clear(); + newCharges = loadBookingNoteCharges( note.getId(), incomes, expenses, nature ); + + if( newCharges.size() > 0 ) { + // 状态改为录入中 + note.setState( BookingNoteChargeState.setEntering( note.getState(), nature ) ); + bookingNoteDao.updateBookingNoteState( note ); + } else if( oldCharges.size() > 0 && newCharges.size() == 0 ) { + // 状态改为还未录入 + note.setState( BookingNoteChargeState.setUnentering( note.getState(), nature ) ); + bookingNoteDao.updateBookingNoteState( note ); + } + + // 更新记账月份 + if( FinanceModel.isNative( nature ) ) { + // 国内费用才更改记账日期 + bookingNoteDao.updateBookingNoteChargeDate( note ); + } + + return newCharges; + } + + private boolean needUpdate( Charge oldCharge, Charge newCharge ) { + if( newCharge.getAmount() - oldCharge.getAmount() > 0.001 ) { + return true; // 金额发生变动 + } else if( oldCharge.getAmount() - newCharge.getAmount() > 0.001 ) { + return true; // 金额发生变动 + } else if( ! oldCharge.getCurrency().getId().equals( newCharge.getCurrency().getId() ) ) { + return true; // 币种发生变动 + } else if( ! oldCharge.getNote().equals( newCharge.getNote() ) ) { + return true; // 备注发生变动 + } else if( ! oldCharge.getType().equals( newCharge.getType() ) ) { + return true; // 费用项目变动 + } else if( ! oldCharge.getTarget().getId().equals( newCharge.getTarget().getId() ) ) { + return true; // 结算对象变动 + } else if( oldCharge.getNature() != newCharge.getNature() ) { + return true; // 境内/境外变动 + } + return false; + } + + private Map getChargeMap( List list ) { + Map map = new HashMap(); + for( Charge charge : list ) { + if( charge != null && StringUtils.hasText( charge.getId() ) ) { + map.put( charge.getId(), charge ); + } + } + return map; + } + + public void setChargeDao( ChargeDao chargeDao ) { + this.chargeDao = chargeDao; + } + + public void setBookingNoteDao( BookingNoteDao bookingNoteDao ) { + this.bookingNoteDao = bookingNoteDao; + } +} diff --git a/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/InvoiceServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/InvoiceServiceImpl.java new file mode 100644 index 0000000..2887c4d --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/java/com/ksa/service/finance/impl/InvoiceServiceImpl.java @@ -0,0 +1,114 @@ +package com.ksa.service.finance.impl; + +import com.ksa.dao.finance.AccountDao; +import com.ksa.dao.finance.InvoiceDao; +import com.ksa.model.ModelUtils; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.AccountState; +import com.ksa.model.finance.FinanceModel; +import com.ksa.model.finance.Invoice; +import com.ksa.service.finance.InvoiceService; +import com.ksa.util.StringUtils; + + +public class InvoiceServiceImpl implements InvoiceService { + + private InvoiceDao invoiceDao; + private AccountDao accountDao; + + + @Override + public Invoice loadInvoiceById( String id ) throws RuntimeException { + Invoice temp = invoiceDao.selectInvoiceById( id ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的发票不存在。", id ) ); + } + return temp; + } + + @Override + public Invoice saveInvoice( Invoice invoice ) throws RuntimeException { + if( StringUtils.hasText( invoice.getId() ) ) { + Invoice temp = invoiceDao.selectInvoiceById( invoice.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的发票不存在。", invoice.getId() ) ); + } + invoiceDao.updateInvoice( invoice ); + } else { + invoice.setId( ModelUtils.generateRandomId() ); + invoiceDao.insertInvoice( invoice ); + } + return invoice; + } + + @Override + public Invoice assignInvoiceToAccount( Invoice invoice, Account account ) throws RuntimeException { + Invoice temp = invoiceDao.selectInvoiceById( invoice.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的发票不存在。", invoice.getId() ) ); + } + + // 如果发票已经对账过,判断是否可以变更 + if( temp.getAccount() != null && AccountState.isSettled( temp.getAccount().getState() ) ) { + throw new IllegalStateException( String.format( "编号为 '%s' 的结算单已经结算完毕,不能进行销账。", temp.getCode() ) ); + } + + // 取消对账 + if( account == null || !StringUtils.hasText( account.getId() ) ) { + temp.setAccount( new Account() ); // 保证 account.id 为空 + invoiceDao.updateInvoiceAccount( temp ); + } else { // 对账 + String accountId = account.getId(); + if( ! accountId.equals( temp.getAccount().getId() ) ) { + account = accountDao.selectAccountById( accountId ); + if( account == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的结算单不存在,无法进行销账。", accountId ) ); + } + if( temp.getDirection() == account.getDirection() ) { + // 收到的发票只能与对账单(支出) 进行销账 + // 反之,开出的发票只能与结算单进行销账 + if( FinanceModel.isIncome( temp ) ) { + throw new IllegalArgumentException( "销账数据错误,收到的发票只能与对账单进行销账!" ); + } else { + throw new IllegalArgumentException( "销账数据错误,开出的发票只能与结算单进行销账!" ); + } + } + + temp.setAccount( account ); + invoiceDao.updateInvoiceAccount( temp ); + } + } + + return temp; + } + + @Override + public Invoice removeInvoice( Invoice invoice ) throws RuntimeException { + Invoice temp = invoiceDao.selectInvoiceById( invoice.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的发票不存在。", invoice.getId() ) ); + } + + // 当发票拿去销账的对应结算单已经确认结算完毕后,不能删除 + if( ModelUtils.isPersistentObject( temp.getAccount() ) && AccountState.isSettled( temp.getAccount().getState() ) ) { + throw new IllegalStateException( String.format( "发票号为 '%s' 的发票已经完成销账,无法删除。", temp.getCode() ) ); + } + + // 删除 + int length = invoiceDao.deleteInvoice( temp ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除发票发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + + return temp; + } + + public void setInvoiceDao( InvoiceDao invoiceDao ) { + this.invoiceDao = invoiceDao; + } + + public void setAccountDao( AccountDao accountDao ) { + this.accountDao = accountDao; + } + +} diff --git a/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/resources/spring/service/finance-service-context.xml b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/resources/spring/service/finance-service-context.xml new file mode 100644 index 0000000..82f8804 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-finance-service/src/main/resources/spring/service/finance-service-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-service-root/ksa-logistics-service/pom.xml b/test_input/ksa/ksa-service-root/ksa-logistics-service/pom.xml new file mode 100644 index 0000000..adc624c --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-logistics-service/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + + com.ksa + ksa-service-root + 3.9.0 + + + ksa-logistics-service + jar + + ksa-logistics-service + 杭州凯思爱物流管理系统 - 物流数据管理 Service 模块 + + + + com.ksa + ksa-logistics-dao + ${project.version} + + + diff --git a/test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/java/com/ksa/service/logistics/impl/BookingNoteServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/java/com/ksa/service/logistics/impl/BookingNoteServiceImpl.java new file mode 100644 index 0000000..1ae3cbb --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/java/com/ksa/service/logistics/impl/BookingNoteServiceImpl.java @@ -0,0 +1,299 @@ +package com.ksa.service.logistics.impl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.ObjectUtils; +import org.springframework.util.StringUtils; + +import com.ksa.dao.logistics.BookingNoteCargoDao; +import com.ksa.dao.logistics.BookingNoteDao; +import com.ksa.model.ModelUtils; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.logistics.BookingNoteCargo; +import com.ksa.model.logistics.BookingNoteState; +import com.ksa.service.logistics.BookingNoteService; + + +public class BookingNoteServiceImpl implements BookingNoteService { + + private static final String BOOKING_NOTE_CODE_PREFIX = "KH"; + + protected BookingNoteDao bookingNoteDao; + protected BookingNoteCargoDao bookingNoteCargoDao; + + @Override + public BookingNote loadBookingNoteById( String id ) throws RuntimeException { + BookingNote note = bookingNoteDao.selectBookingNoteById( id ); + if( note == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的托单不存在。", id ) ); + } + return note; + } + + @Override + public BookingNote getNewBookingNote( String type ) throws RuntimeException { + BookingNote bn = new BookingNote(); + if( BookingNote.TYPE_SEA_EXPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_SEA_EXPORT ); + bn.setSerialNumber( queryNotNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_SEA_IMPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_SEA_IMPORT ); + bn.setSerialNumber( queryNotNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_AIR_EXPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_AIR_EXPORT ); + bn.setSerialNumber( queryNotNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_AIR_IMPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_AIR_IMPORT ); + bn.setSerialNumber( queryNotNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_NATIVE.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_NATIVE ); + bn.setSerialNumber( queryNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_ZJ.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_ZJ ); + bn.setSerialNumber( queryNotNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_KB.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_KB ); + bn.setSerialNumber( queryNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_BC.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_BC ); + bn.setSerialNumber( queryNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_CC.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_CC ); + bn.setSerialNumber( queryNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_RH.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_RH ); + bn.setSerialNumber( queryNativeBookingNoteCount() + 1 ); + } else if( BookingNote.TYPE_TL.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_TL ); + bn.setSerialNumber( queryNativeBookingNoteCount() + 1 ); + } else { + throw new IllegalArgumentException( String.format( "不存在类型为 '%s' 的托单。", type ) ); + } + bn.setCode( BOOKING_NOTE_CODE_PREFIX + bn.getType() + bn.getSerialNumber() ); + + return bn; + } + + private int queryNativeBookingNoteCount() { + return bookingNoteDao.selectBookingNoteCount( " TYPE = 'LY' " ); + } + + private int queryNotNativeBookingNoteCount() { + return bookingNoteDao.selectBookingNoteCount( " TYPE <> 'LY' " ); + } + + @Override + public BookingNote createBookingNote( BookingNote note ) throws RuntimeException { + BookingNote temp = getNewBookingNote( note.getType() ); + + // 2013-09-19 v3.4.6 加入对提单号的唯一性验证 + if( isLadingNoExist( note ) ) { + note.setId( null ); + BookingNote lading = bookingNoteDao.selectBookingNoteByLading( note ); + if( lading != null ) { + if( lading.getMawb() != null && lading.getMawb().equals( note.getMawb() ) ) { + throw new IllegalStateException( String.format( "主提单号 '%s' 已存在,详见托单 '%s'。请勿为同一提单重复创建托单。", lading.getMawb(), lading.getCode() ) ); + } else { + throw new IllegalStateException( String.format( "副提单号 '%s' 已存在,详见托单 '%s'。请勿为同一提单重复创建托单。", lading.getHawb(), lading.getCode() ) ); + } + } + } + + // 基本数据的初始化 + String id = ModelUtils.generateRandomId(); + note.setType( temp.getType() ); + note.setSerialNumber( temp.getSerialNumber() ); + note.setCode( temp.getCode() ); + note.setId( id ); + + int length = bookingNoteDao.insertBookingNote( note ); + if( length != 1 ) { + String errorMessage = "新增%s发生异常,期望新增 1 条数据,实际新增了 %d 条数据。"; + throw new IllegalStateException( String.format( errorMessage, "托单", length ) ); + } + + // 添加 Cargo + List cargos = note.getCargos(); + if( cargos != null && cargos.size() > 0 ) { + for( BookingNoteCargo cargo : cargos ) { + cargo.setId( ModelUtils.generateRandomId() ); + cargo.setBookingNote( note ); + bookingNoteCargoDao.insertCargo( cargo ); + } + } + + return note; + } + + @Override + public BookingNote modifyBookingNote( BookingNote note ) throws RuntimeException { + String id = note.getId(); + BookingNote temp = bookingNoteDao.selectBookingNoteById( id ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的托单不存在。", note.getId() ) ); + } + + // 2013-09-19 v3.4.6 加入对提单号的唯一性验证 + if( isLadingNoExist( note ) ) { + BookingNote lading = bookingNoteDao.selectBookingNoteByLading( note ); + if( lading != null ) { + if( lading.getMawb() != null && lading.getMawb().equals( note.getMawb() ) ) { + throw new IllegalStateException( String.format( "主提单号 '%s' 已存在,详见托单 '%s'。请勿为同一提单重复创建托单。", lading.getMawb(), lading.getCode() ) ); + } else { + throw new IllegalStateException( String.format( "副提单号 '%s' 已存在,详见托单 '%s'。请勿为同一提单重复创建托单。", lading.getHawb(), lading.getCode() ) ); + } + } + } + + // 完成退单状态的设置 + if( note.getState() == BookingNoteState.RETURNED ) { + temp.setState( BookingNoteState.setReturned( temp.getState() ) ); + bookingNoteDao.updateBookingNoteState( note ); + } + + int length = bookingNoteDao.updateBookingNote( note ); + if( length != 1 ) { + String errorMessage = "更新%s发生异常,期望新增 1 条数据,实际更新了 %d 条数据。"; + throw new IllegalStateException( String.format( errorMessage, "托单", length ) ); + } + + + + // 更新 Cargo :新增以前没有的,修改变更的,删除现在没有的。 + if( note.getCargos() != null && note.getCargos().size() > 0 ) { + for( BookingNoteCargo cargo : note.getCargos() ) { + if( ! StringUtils.hasText( cargo.getId() ) ) { + // 新增 + BookingNoteCargo newCargo = new BookingNoteCargo(); + + newCargo.setId( ModelUtils.generateRandomId() ); + newCargo.setBookingNote( note ); + newCargo.setName( cargo.getName() ); + newCargo.setType( cargo.getType() ); + newCargo.setCategory( cargo.getCategory() ); + newCargo.setAmount( cargo.getAmount() ); + bookingNoteCargoDao.insertCargo( newCargo ); + } + } + } + + Map oldCargos = generateCargoMap( temp.getCargos() ); + Map newCargos = generateCargoMap( note.getCargos() ); + + for( String oldKey : oldCargos.keySet() ) { + if( newCargos.containsKey( oldKey ) ) { + // 更新 + BookingNoteCargo newCargo = newCargos.get( oldKey ); + if( needUpdate( newCargo, oldCargos.get( oldKey ) ) ) { + newCargo.setBookingNote( note ); + bookingNoteCargoDao.updateCargo( newCargo ); + } + } else { + // 移除 + bookingNoteCargoDao.deleteCargo( oldCargos.get( oldKey ) ); + } + } + + return note; + } + + private boolean needUpdate( BookingNoteCargo cargo1, BookingNoteCargo cargo2 ) { + if( cargo1.getAmount() != cargo2.getAmount() ) { + return true; + } else if( ObjectUtils.notEqual( cargo1.getCategory(), cargo2.getCategory() ) ) { + return true; + } else if( ObjectUtils.notEqual( cargo1.getName(), cargo2.getName() ) ) { + return true; + } else if( ObjectUtils.notEqual( cargo1.getType(), cargo2.getType() ) ) { + return true; + } + return false; + } + + private Map generateCargoMap( List cargos ) { + Map cargoMap = new HashMap(); + if( cargos != null && cargos.size() > 0 ) { + for( BookingNoteCargo cargo : cargos ) { + if( StringUtils.hasText( cargo.getId() ) ) { + cargoMap.put( cargo.getId(), cargo ); + } + } + } + return cargoMap; + } + + @Override + public BookingNote removeBookingNote( BookingNote note ) throws RuntimeException { + BookingNote temp = bookingNoteDao.selectBookingNoteById( note.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的托单不存在。", note.getId() ) ); + } + // 将托单标记为 '已删除' 状态,实际并未真正删除。 + int length = bookingNoteDao.deleteBookingNote( temp ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除托单发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", length ) ); + } + + return temp; + } + + public BookingNote changeBookingNoteType( BookingNote note ) throws RuntimeException { + BookingNote bn = bookingNoteDao.selectBookingNoteById( note.getId() ); + if( bn == null ) { + throw new IllegalArgumentException( String.format( "标识为 '%s' 的托单不存在。", note.getId() ) ); + } + + String type = note.getType(); + if( BookingNote.TYPE_SEA_EXPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_SEA_EXPORT ); + } else if( BookingNote.TYPE_SEA_IMPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_SEA_IMPORT ); + } else if( BookingNote.TYPE_AIR_EXPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_AIR_EXPORT ); + } else if( BookingNote.TYPE_AIR_IMPORT.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_AIR_IMPORT ); + } else if( BookingNote.TYPE_NATIVE.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_NATIVE ); + } else if( BookingNote.TYPE_KB.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_KB ); + } else if( BookingNote.TYPE_BC.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_BC ); + } else if( BookingNote.TYPE_CC.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_CC ); + } else if( BookingNote.TYPE_RH.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_RH ); + } else if( BookingNote.TYPE_TL.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_TL ); + } else if( BookingNote.TYPE_ZJ.equalsIgnoreCase( type ) ) { + bn.setType( BookingNote.TYPE_ZJ ); + } else { + throw new IllegalArgumentException( String.format( "不存在类型为 '%s' 的托单。", type ) ); + } + bn.setCode( BOOKING_NOTE_CODE_PREFIX + bn.getType() + bn.getSerialNumber() ); + + // 变更托单类型 + int length = bookingNoteDao.updateBookingNoteType( bn ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "变更托单类型时发生异常,期望变更 1 条数据,实际变更了 %d 条数据。", length ) ); + } + + return bn; + } + + + public void setBookingNoteDao( BookingNoteDao bookingNoteDao ) { + this.bookingNoteDao = bookingNoteDao; + } + + public void setBookingNoteCargoDao( BookingNoteCargoDao bookingNoteCargoDao ) { + this.bookingNoteCargoDao = bookingNoteCargoDao; + } + + // 判断是否存在提单号,主副提单有其一即可 + private boolean isLadingNoExist( BookingNote note ) { + return StringUtils.hasText( note.getMawb() ) || StringUtils.hasText( note.getHawb() ) ; + } + +} diff --git a/test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/resources/spring/service/logistics-service-context.xml b/test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/resources/spring/service/logistics-service-context.xml new file mode 100644 index 0000000..3446f0b --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-logistics-service/src/main/resources/spring/service/logistics-service-context.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-service-root/ksa-security-service/pom.xml b/test_input/ksa/ksa-service-root/ksa-security-service/pom.xml new file mode 100644 index 0000000..dd51e97 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-security-service/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + + com.ksa + ksa-service-root + 3.9.0 + + + ksa-security-service + jar + + ksa-security-service + 杭州凯思爱物流管理系统 - 安全管理 Service 模块 + + + + com.ksa + ksa-security-dao + ${project.version} + + + + org.apache.shiro + shiro-core + 1.2.0 + + + org.apache.shiro + shiro-ehcache + 1.2.0 + + + diff --git a/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/impl/SecurityServiceImpl.java b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/impl/SecurityServiceImpl.java new file mode 100644 index 0000000..03e9def --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/impl/SecurityServiceImpl.java @@ -0,0 +1,249 @@ +package com.ksa.service.security.impl; + +import java.util.UUID; + +import com.ksa.dao.security.RoleDao; +import com.ksa.dao.security.UserDao; +import com.ksa.model.security.Role; +import com.ksa.model.security.User; +import com.ksa.service.security.SecurityService; +import com.ksa.service.security.util.SecurityUtils; + +/** + * 系统安全服务默认实现。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class SecurityServiceImpl implements SecurityService { + + private UserDao userDao; + private RoleDao roleDao; + + @Override + public User loadUserById( String userId ) throws RuntimeException { + User user = userDao.selectUserById( userId ); + if( user == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的用户不存在。", userId ) ); + } + return user; + } + + @Override + public User createUser( User user ) throws RuntimeException { + return createUser( user, null ); + } + + @Override + public User createUser( User user, String[] roleIds ) throws RuntimeException { + User temp = userDao.selectUserById( user.getId() ); + if( temp != null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的用户已经存在。", user.getId() ) ); + } + + // 散列保存密码 + user.setPassword( SecurityUtils.digestPassword( user.getPassword() ) ); + + int length = userDao.insertUser( user ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "新增用户数据发生异常,期望新增 1 条数据,实际新增了 %d 条数据。", user.getId() ) ); + } + + String userId = user.getId(); + // 插入初始角色 + if( roleIds != null && roleIds.length > 0 ) { + for( String roleId : roleIds ) { + userDao.insertUserRole( userId, roleId ); + } + } + + return user; + } + + @Override + public User lockUser( User user ) throws RuntimeException { + return doLockUser( user, true ); + } + + @Override + public User unlockUser( User user ) throws RuntimeException { + return doLockUser( user, false ); + } + + private User doLockUser( User user, boolean isLock ) throws RuntimeException { + User temp = userDao.selectUserById( user.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的用户不存在。", user.getId() ) ); + } + + temp.setLocked( isLock ); + int length = userDao.updateUser( temp ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "锁定/激活用户数据发生异常,期望操作 1 条数据,实际操作了 %d 条数据。", user.getId() ) ); + } + + return temp; + } + + @Override + public User modifyUser( User user ) throws RuntimeException { + User temp = userDao.selectUserById( user.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的用户不存在。", user.getId() ) ); + } + + // 不更新密码 + user.setPassword( temp.getPassword() ); + int length = userDao.updateUser( user ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新用户数据发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", user.getId() ) ); + } + + return user; + } + + @Override + public User modifyUser( String userId, String oldPassword, String newPassword ) throws RuntimeException { + User temp = userDao.selectUserById( userId ); + + if( temp == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的用户不存在。", userId ) ); + } + + if( ! temp.getPassword().equals( SecurityUtils.digestPassword( oldPassword ) ) ) { + throw new IllegalArgumentException( "用户原密码输入有误。" ); + } + + // 新密码 + temp.setPassword( SecurityUtils.digestPassword( newPassword ) ); + int length = userDao.updateUser( temp ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新用户密码发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", userId ) ); + } + + return temp; + } + + @Override + public User modifyUser( User user, String oldPassword, String newPassword ) throws RuntimeException { + User temp = userDao.selectUserById( user.getId() ); + + if( temp == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的用户不存在。", user.getId() ) ); + } + + if( ! temp.getPassword().equals( SecurityUtils.digestPassword( oldPassword ) ) ) { + throw new IllegalArgumentException( "用户原密码输入有误。" ); + } + + // 新密码 + user.setPassword( SecurityUtils.digestPassword( newPassword ) ); + int length = userDao.updateUser( user ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新用户数据发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", user.getId() ) ); + } + + return user; + } + + @Override + public User removeUser( User user ) throws RuntimeException { + User temp = userDao.selectUserById( user.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的用户不存在。", user.getId() ) ); + } + + // TODO 其他判断用户是否可以被删除的逻辑 + int length = userDao.deleteUser( user ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除用户数据发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", user.getId() ) ); + } + + return temp; + } + + //------------------- 角色相关操作 -------------------------------// + + @Override + public Role loadRoleById( String roleId ) throws RuntimeException { + Role role = roleDao.selectRoleById( roleId ); + if( role == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的角色不存在。", roleId ) ); + } + return role; + } + + @Override + public Role createRole( Role role ) throws RuntimeException { + return createRole( role, null, null ); + } + + @Override + public Role createRole( Role role, String[] userIds, String[] permissionIds ) throws RuntimeException { + + // 初始化角色的标识 + String roleId = UUID.randomUUID().toString(); + role.setId( roleId ); + + int length = roleDao.insertRole( role ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "新增角色数据发生异常,期望新增 1 条数据,实际新增了 %d 条数据。", role.getId() ) ); + } + + // 插入初始用户 + if( userIds != null && userIds.length > 0 ) { + for( String userId : userIds ) { + userDao.insertUserRole( userId, roleId ); + } + } + // 插入初始权限 + if( permissionIds != null && permissionIds.length > 0 ) { + for( String permissionId : permissionIds ) { + roleDao.insertRolePermission( roleId, permissionId ); + } + } + + return role; + } + + @Override + public Role modifyRole( Role role ) throws RuntimeException { + Role temp = roleDao.selectRoleById( role.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的角色不存在。", role.getId() ) ); + } + + int length = roleDao.updateRole( role ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "更新角色数据发生异常,期望更新 1 条数据,实际更新了 %d 条数据。", role.getId() ) ); + } + + return role; + } + + @Override + public Role removeRole( Role role ) throws RuntimeException { + Role temp = roleDao.selectRoleById( role.getId() ); + if( temp == null ) { + throw new IllegalArgumentException( String.format( "编号为 '%s' 的角色不存在。", role.getId() ) ); + } + + int length = roleDao.deleteRole( role ); + if( length != 1 ) { + throw new IllegalStateException( String.format( "删除角色数据发生异常,期望删除 1 条数据,实际删除了 %d 条数据。", role.getId() ) ); + } + + return temp; + } + + + + public void setUserDao( UserDao userDao ) { + this.userDao = userDao; + } + + public void setRoleDao( RoleDao roleDao ) { + this.roleDao = roleDao; + } +} diff --git a/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/PasswordMatcher.java b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/PasswordMatcher.java new file mode 100644 index 0000000..d194b41 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/PasswordMatcher.java @@ -0,0 +1,24 @@ +package com.ksa.service.security.shiro; + +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.credential.CredentialsMatcher; + +import com.ksa.service.security.util.SecurityUtils; + +public class PasswordMatcher implements CredentialsMatcher { + + @Override + public boolean doCredentialsMatch( AuthenticationToken token, AuthenticationInfo info ) { + Object credentials = token.getCredentials(); + String inputPassword = null; + if( credentials.getClass().isArray() ) { + inputPassword = new String( (char[]) credentials ); + } else { + inputPassword = credentials.toString(); + } + String digestPassword = SecurityUtils.digestPassword( inputPassword ); + return info.getCredentials().equals( digestPassword ); + } + +} diff --git a/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/ShiroRealm.java b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/ShiroRealm.java new file mode 100644 index 0000000..12454f3 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/shiro/ShiroRealm.java @@ -0,0 +1,63 @@ +package com.ksa.service.security.shiro; + +import java.util.List; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.LockedAccountException; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; + +import com.ksa.model.security.Permission; +import com.ksa.model.security.Role; +import com.ksa.model.security.User; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.util.StringUtils; + +public class ShiroRealm extends AuthorizingRealm { + + @Override + protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals ) { + User user = ( User ) principals.getPrimaryPrincipal(); + + if( user != null ) { + user = SecurityUtils.getUser( user.getId() ); + List roles = user.getRoles(); + if( roles != null && roles.size() > 0 ) { + SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); + for( Role role : roles ) { + info.addRole( role.getName() ); + for( Permission p : role.getPermissions() ) { + info.addStringPermission( p.getId() ); + } + } + return info; + } + } + + return null; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken ) throws AuthenticationException { + UsernamePasswordToken token = (UsernamePasswordToken)authcToken; + String userId = token.getUsername(); + // 用户名密码验证 + if( StringUtils.hasText( userId ) ) { + User user = SecurityUtils.getUser( userId ); + if( user != null ) { + if( user.isLocked() ) { + throw new LockedAccountException( String.format( "用户 '%s' 已被锁定。", userId ) ); + } + return new SimpleAuthenticationInfo( user, user.getPassword(), getName() ); + } + } + return null; + } + +} \ No newline at end of file diff --git a/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/util/SecurityUtils.java b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/util/SecurityUtils.java new file mode 100644 index 0000000..a84ed89 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/java/com/ksa/service/security/util/SecurityUtils.java @@ -0,0 +1,87 @@ +package com.ksa.service.security.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.dao.security.UserDao; +import com.ksa.model.security.User; +import com.ksa.util.codec.Base64; + + +public class SecurityUtils { + + private static final Logger log = LoggerFactory.getLogger( SecurityUtils.class ); + + private static UserDao userDao; + private static Map cache = new HashMap(); // 用户缓存 + + static { + userDao = ServiceContextUtils.getService( UserDao.class ); + } + + /** + * 获取当前登录的用户 + * @return + */ + public static User getCurrentUser() { + return org.apache.shiro.SecurityUtils.getSubject().getPrincipals().oneByType( User.class ); + } + + /** + * 通过用户标识获取用户信息。 + * @param id 用户标识 + * @return 返回对应标识的用户,如果用户不存在则返回 null 。 + */ + public static User getUser( String id ) { + if( cache.containsKey( id ) ) { + return cache.get( id ); + } + User user = userDao.selectUserById( id ); + if( user != null ) { + cache.put( id, user ); + return user; + } + return null; + } + + /** + * 获取用户缓存 + * @return + */ + public static Map getCache() { + return cache; + } + + public static String digestPassword( String password ) { + try { + MessageDigest md = MessageDigest.getInstance( "SHA-1" ); + byte[] passBytes = md.digest( Base64.convertToBytes( password ) ); + return Base64.convertToString( Base64.encode( passBytes ) ); + } catch( NoSuchAlgorithmException e ) { + log.warn( "获取消息散列算法工具失败。", e ); + return password; + } + } + + /** + * Returns {@code true} if this Subject is permitted to perform an action or access a resource summarized by the + * specified permission string. + *

+ * This is an overloaded method for the corresponding type-safe {@link Permission Permission} variant. + * Please see the class-level JavaDoc for more information on these String-based permission methods. + * + * @param permission the String representation of a Permission that is being checked. + * @return true if this Subject is permitted, false otherwise. + * @see #isPermitted(Permission permission) + * @since 0.9 + */ + public static boolean isPermitted( String permission ) { + return org.apache.shiro.SecurityUtils.getSubject().isPermitted( permission ); + } +} diff --git a/test_input/ksa/ksa-service-root/ksa-security-service/src/main/resources/spring/service/security-service-context.xml b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/resources/spring/service/security-service-context.xml new file mode 100644 index 0000000..3893786 --- /dev/null +++ b/test_input/ksa/ksa-service-root/ksa-security-service/src/main/resources/spring/service/security-service-context.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-service-root/pom.xml b/test_input/ksa/ksa-service-root/pom.xml new file mode 100644 index 0000000..c570e8c --- /dev/null +++ b/test_input/ksa/ksa-service-root/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + + com.ksa + ksa-root + 3.9.0 + + + ksa-service-root + pom + + ksa-service-root + 杭州凯思爱物流管理系统 - Service 模块根模型 + + + UTF-8 + + + + + com.ksa + ksa-core + ${project.version} + + + + + ksa-security-service + ksa-bd-service + ksa-logistics-service + ksa-finance-service + + diff --git a/test_input/ksa/ksa-web-core/pom.xml b/test_input/ksa/ksa-web-core/pom.xml new file mode 100644 index 0000000..3d2012c --- /dev/null +++ b/test_input/ksa/ksa-web-core/pom.xml @@ -0,0 +1,91 @@ + + 4.0.0 + + + com.ksa + ksa-root + 3.9.0 + + + ksa-web-core + jar + + ksa-web-core + 杭州凯思爱物流管理系统 - WEB 核心模块 + + + UTF-8 + 1.2.0 + 2.3.31 + + + + + + org.apache.geronimo.specs + geronimo-servlet_2.5_spec + + + + org.apache.geronimo.specs + geronimo-jsp_2.1_spec + + + + + + org.springframework + spring-web + + + + + org.apache.struts + struts2-core + ${struts.version} + + + org.apache.struts + struts2-json-plugin + ${struts.version} + + + + + ro.isdc.wro4j + wro4j-core + 1.4.0 + + + + org.apache.shiro + shiro-web + ${shiro.version} + + + + + com.ksa + ksa-core + ${project.version} + + + com.ksa + ksa-dao-context + 3.9.0 + + + commons-configuration + commons-configuration + 1.9 + + + + + github + GitHub OWNER Maven Packages + https://maven.registry.github.com/whitesource + + + diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/RuntimeConfiguration.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/RuntimeConfiguration.java new file mode 100644 index 0000000..e1481f3 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/RuntimeConfiguration.java @@ -0,0 +1,38 @@ +package com.ksa.context.web; + +import java.net.URL; + +import javax.servlet.ServletContext; + +import org.apache.commons.configuration.FileConfiguration; +import org.apache.commons.configuration.PropertiesConfiguration; + +public class RuntimeConfiguration { + + private static FileConfiguration config; + + private static ServletContext context; + + public static FileConfiguration getConfiguration() { + return config; + } + + static void init( ServletContext servletContext ) { + context = servletContext; + try { + URL url = servletContext.getResource( "/WEB-INF/runtime.properties" ); + config = new PropertiesConfiguration( url ); + } catch( Exception e ) { + throw new RuntimeException( "运行时配置初始化失败。", e ); + } + } + + public static void save() { + try { + URL url = context.getResource( "/WEB-INF/runtime.properties" ); + config.save( url ); + } catch( Exception e ) { + throw new RuntimeException( "运行时配置初始化失败。", e ); + } + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/SpringServiceContextListener.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/SpringServiceContextListener.java new file mode 100644 index 0000000..1c1ce5b --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/context/web/SpringServiceContextListener.java @@ -0,0 +1,44 @@ +package com.ksa.context.web; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.ContextLoader; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.context.spring.SpringServiceContext; + +/** + * 重写spring原生的ContextLoaderListener类。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class SpringServiceContextListener extends ContextLoader implements ServletContextListener { + + /* + * (non-Javadoc) + * + * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent) + */ + @Override + public void contextInitialized( ServletContextEvent event ) { + ApplicationContext context = this.initWebApplicationContext( event.getServletContext() ); + ServiceContextUtils.init( new SpringServiceContext( context ) ); + + RuntimeConfiguration.init( event.getServletContext() ); + } + + /* + * (non-Javadoc) + * + * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent) + */ + @Override + public void contextDestroyed( ServletContextEvent event ) { + this.closeWebApplicationContext( event.getServletContext() ); + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/AuthenticatedTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/AuthenticatedTag.java new file mode 100644 index 0000000..50dcfd5 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/AuthenticatedTag.java @@ -0,0 +1,45 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.log.Logger; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateException; + +import java.io.IOException; +import java.util.Map; + + +/** + * JSP tag that renders the tag body only if the current user has executed a successful authentication attempt + * during their current session. + * + *

This is more restrictive than the {@link UserTag}, which only + * ensures the current user is known to the system, either via a current login or from Remember Me services, + * which only makes the assumption that the current user is who they say they are, and does not guarantee it like + * this tag does. + * + *

The logically opposite tag of this one is the {@link NotAuthenticatedTag} + * + *

Equivalent to {@link org.apache.shiro.web.tags.AuthenticatedTag}

+ * + * @since 0.2 + */ +public class AuthenticatedTag extends SecureTag { + private static final Logger log = Logger.getLogger("AuthenticatedTag"); + + @SuppressWarnings( "rawtypes" ) + @Override + public void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException { + if (getSubject() != null && getSubject().isAuthenticated()) { + if (log.isDebugEnabled()) { + log.debug("Subject exists and is authenticated. Tag body will be evaluated."); + } + + renderBody(env, body); + } else { + if (log.isDebugEnabled()) { + log.debug("Subject does not exist or is not authenticated. Tag body will not be evaluated."); + } + } + } +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/GuestTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/GuestTag.java new file mode 100644 index 0000000..ffcb320 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/GuestTag.java @@ -0,0 +1,43 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.log.Logger; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateException; + +import java.io.IOException; +import java.util.Map; + + +/** + * JSP tag that renders the tag body if the current user is not known to the system, either because they + * haven't logged in yet, or because they have no 'RememberMe' identity. + * + *

The logically opposite tag of this one is the {@link UserTag}. Please read that class's JavaDoc as it explains + * more about the differences between Authenticated/Unauthenticated and User/Guest semantic differences. + * + *

Equivalent to {@link org.apache.shiro.web.tags.GuestTag}

+ * + * @since 0.9 + */ +public class GuestTag extends SecureTag { + private static final Logger log = Logger.getLogger("AuthenticatedTag"); + + @SuppressWarnings( "rawtypes" ) + @Override + public void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException { + if (getSubject() == null || getSubject().getPrincipal() == null) { + if (log.isDebugEnabled()) { + log.debug("Subject does not exist or does not have a known identity (aka 'principal'). " + + "Tag body will be evaluated."); + } + + renderBody(env, body); + } else { + if (log.isDebugEnabled()) { + log.debug("Subject exists or has a known identity (aka 'principal'). " + + "Tag body will not be evaluated."); + } + } + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyPermissionsTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyPermissionsTag.java new file mode 100644 index 0000000..6ca7bfd --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyPermissionsTag.java @@ -0,0 +1,28 @@ +package com.ksa.shiro.freemarker; + +import org.apache.shiro.subject.Subject; + +public class HasAnyPermissionsTag extends PermissionTag { + + // Delimeter that separates role names in tag attribute + private static final String PERMISSION_NAMES_DELIMETER = ","; + + protected boolean showTagBody( String permissions ) { + boolean hasAnyPermission = false; + + Subject subject = getSubject(); + + if( subject != null ) { + // Iterate through roles and check to see if the user has one of the roles + for( String p : permissions.split( PERMISSION_NAMES_DELIMETER ) ) { + if( subject.isPermitted( p.trim() ) ) { + hasAnyPermission = true; + break; + } + } + } + + return hasAnyPermission; + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyRolesTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyRolesTag.java new file mode 100644 index 0000000..7127e5d --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasAnyRolesTag.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ksa.shiro.freemarker; + +import org.apache.shiro.subject.Subject; + + +/** + * Displays body content if the current user has any of the roles specified. + * + *

Equivalent to {@link org.apache.shiro.web.tags.HasAnyRolesTag}

+ * + * @since 0.2 + */ +public class HasAnyRolesTag extends RoleTag { + // Delimeter that separates role names in tag attribute + private static final String ROLE_NAMES_DELIMETER = ","; + + protected boolean showTagBody(String roleNames) { + boolean hasAnyRole = false; + Subject subject = getSubject(); + + if (subject != null) { + // Iterate through roles and check to see if the user has one of the roles + for (String role : roleNames.split(ROLE_NAMES_DELIMETER)) { + if (subject.hasRole(role.trim())) { + hasAnyRole = true; + break; + } + } + } + + return hasAnyRole; + } +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasPermissionTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasPermissionTag.java new file mode 100644 index 0000000..002169c --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasPermissionTag.java @@ -0,0 +1,12 @@ +package com.ksa.shiro.freemarker; + +/** + *

Equivalent to {@link org.apache.shiro.web.tags.HasPermissionTag}

+ * + * @since 0.1 + */ +public class HasPermissionTag extends PermissionTag { + protected boolean showTagBody(String p) { + return isPermitted(p); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasRoleTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasRoleTag.java new file mode 100644 index 0000000..71dd1b4 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/HasRoleTag.java @@ -0,0 +1,10 @@ +package com.ksa.shiro.freemarker; + +/** + *

Equivalent to {@link org.apache.shiro.web.tags.HasRoleTag}

+ */ +public class HasRoleTag extends RoleTag { + protected boolean showTagBody(String roleName) { + return getSubject() != null && getSubject().hasRole(roleName); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksPermissionTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksPermissionTag.java new file mode 100644 index 0000000..f87a914 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksPermissionTag.java @@ -0,0 +1,10 @@ +package com.ksa.shiro.freemarker; + +/** + *

Equivalent to {@link org.apache.shiro.web.tags.LacksPermissionTag}

+ */ +public class LacksPermissionTag extends PermissionTag { + protected boolean showTagBody(String p) { + return !isPermitted(p); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksRoleTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksRoleTag.java new file mode 100644 index 0000000..990b170 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/LacksRoleTag.java @@ -0,0 +1,11 @@ +package com.ksa.shiro.freemarker; + +/** + *

Equivalent to {@link org.apache.shiro.web.tags.LacksRoleTag}

+ */ +public class LacksRoleTag extends RoleTag { + protected boolean showTagBody(String roleName) { + boolean hasRole = getSubject() != null && getSubject().hasRole(roleName); + return !hasRole; + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/NotAuthenticatedTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/NotAuthenticatedTag.java new file mode 100644 index 0000000..91c9382 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/NotAuthenticatedTag.java @@ -0,0 +1,33 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.log.Logger; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateException; + +import java.io.IOException; +import java.util.Map; + + +/** + * Freemarker tag that renders the tag body only if the current user has not executed a successful authentication + * attempt during their current session. + * + *

The logically opposite tag of this one is the {@link org.apache.shiro.web.tags.AuthenticatedTag}. + * + *

Equivalent to {@link org.apache.shiro.web.tags.NotAuthenticatedTag}

+ */ +public class NotAuthenticatedTag extends SecureTag { + static final Logger log = Logger.getLogger("NotAuthenticatedTag"); + + @SuppressWarnings( "rawtypes" ) + @Override + public void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException { + if (getSubject() == null || !getSubject().isAuthenticated()) { + log.debug("Subject does not exist or is not authenticated. Tag body will be evaluated."); + renderBody(env, body); + } else { + log.debug("Subject exists and is authenticated. Tag body will not be evaluated."); + } + } +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PermissionTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PermissionTag.java new file mode 100644 index 0000000..72e2b77 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PermissionTag.java @@ -0,0 +1,45 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateException; +import freemarker.template.TemplateModelException; +import java.io.IOException; +import java.util.Map; + +/** + *

Equivalent to {@link org.apache.shiro.web.tags.PermissionTag}

+ */ +public abstract class PermissionTag extends SecureTag { + @SuppressWarnings( "rawtypes" ) + String getName(Map params) { + return getParam(params, "name"); + } + + @SuppressWarnings( "rawtypes" ) + @Override + protected void verifyParameters(Map params) throws TemplateModelException { + String permission = getName(params); + + if (permission == null || permission.length() == 0) { + throw new TemplateModelException("The 'name' tag attribute must be set."); + } + } + + @SuppressWarnings( "rawtypes" ) + @Override + public void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException { + String p = getName(params); + + boolean show = showTagBody(p); + if (show) { + renderBody(env, body); + } + } + + protected boolean isPermitted(String p) { + return getSubject() != null && getSubject().isPermitted(p); + } + + protected abstract boolean showTagBody(String p); +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PrincipalTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PrincipalTag.java new file mode 100644 index 0000000..e4704d5 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/PrincipalTag.java @@ -0,0 +1,121 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.log.Logger; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateException; +import freemarker.template.TemplateModelException; + +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.IOException; +import java.util.Map; + +/** + *

Tag used to print out the String value of a user's default principal, + * or a specific principal as specified by the tag's attributes.

+ * + *

If no attributes are specified, the tag prints out the toString() + * value of the user's default principal. If the type attribute + * is specified, the tag looks for a principal with the given type. If the + * property attribute is specified, the tag prints the string value of + * the specified property of the principal. If no principal is found or the user + * is not authenticated, the tag displays nothing unless a defaultValue + * is specified.

+ * + *

Equivalent to {@link org.apache.shiro.web.tags.PrincipalTag}

+ * + * @since 0.2 + */ +public class PrincipalTag extends SecureTag { + static final Logger log = Logger.getLogger("PrincipalTag"); + + /** + * The type of principal to be retrieved, or null if the default principal should be used. + */ + @SuppressWarnings( "rawtypes" ) + String getType(Map params) { + return getParam(params, "type"); + } + + /** + * The property name to retrieve of the principal, or null if the toString() value should be used. + */ + @SuppressWarnings( "rawtypes" ) + String getProperty(Map params) { + return getParam(params, "property"); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException { + String result = null; + + if (getSubject() != null) { + // Get the principal to print out + Object principal; + + if (getType(params) == null) { + principal = getSubject().getPrincipal(); + } else { + principal = getPrincipalFromClassName(params); + } + + // Get the string value of the principal + if (principal != null) { + String property = getProperty(params); + + if (property == null) { + result = principal.toString(); + } else { + result = getPrincipalProperty(principal, property); + } + } + } + + // Print out the principal value if not null + if (result != null) { + try { + env.getOut().write(result); + } catch (IOException ex) { + throw new TemplateException("Error writing ["+result+"] to Freemarker.", ex, env); + } + } + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + Object getPrincipalFromClassName(Map params) { + String type = getType(params); + + try { + Class cls = Class.forName(type); + + return getSubject().getPrincipals().oneByType(cls); + } catch (ClassNotFoundException ex) { + log.error("Unable to find class for name ["+type+"]", ex); + } + + return null; + } + + String getPrincipalProperty(Object principal, String property) throws TemplateModelException { + try { + BeanInfo beanInfo = Introspector.getBeanInfo(principal.getClass()); + + // Loop through the properties to get the string value of the specified property + for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) { + if (propertyDescriptor.getName().equals(property)) { + Object value = propertyDescriptor.getReadMethod().invoke(principal, (Object[]) null); + + return String.valueOf(value); + } + } + + // property not found, throw + throw new TemplateModelException("Property ["+property+"] not found in principal of type ["+principal.getClass().getName()+"]"); + } catch (Exception ex) { + throw new TemplateModelException("Error reading property ["+property+"] from principal of type ["+principal.getClass().getName()+"]", ex); + } + } +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/RoleTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/RoleTag.java new file mode 100644 index 0000000..2b50681 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/RoleTag.java @@ -0,0 +1,28 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateException; +import java.io.IOException; +import java.util.Map; + +/** + *

Equivalent to {@link org.apache.shiro.web.tags.RoleTag}

+ */ +public abstract class RoleTag extends SecureTag { + @SuppressWarnings( "rawtypes" ) + String getName(Map params) { + return getParam(params, "name"); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException { + boolean show = showTagBody(getName(params)); + if (show) { + renderBody(env, body); + } + } + + protected abstract boolean showTagBody(String roleName); +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/SecureTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/SecureTag.java new file mode 100644 index 0000000..d46808f --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/SecureTag.java @@ -0,0 +1,47 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.template.*; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import java.io.IOException; +import java.util.Map; + +/** + *

Equivalent to {@link org.apache.shiro.web.tags.SecureTag}

+ */ +public abstract class SecureTag implements TemplateDirectiveModel { + @SuppressWarnings( "rawtypes" ) + public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { + verifyParameters(params); + render(env, params, body); + } + + @SuppressWarnings( "rawtypes" ) + public abstract void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException; + + @SuppressWarnings( "rawtypes" ) + protected String getParam(Map params, String name) { + Object value = params.get(name); + + if (value instanceof SimpleScalar) { + return ((SimpleScalar)value).getAsString(); + } + + return null; + } + + protected Subject getSubject() { + return SecurityUtils.getSubject(); + } + + @SuppressWarnings( "rawtypes" ) + protected void verifyParameters(Map params) throws TemplateModelException { + } + + protected void renderBody(Environment env, TemplateDirectiveBody body) throws IOException, TemplateException { + if (body != null) { + body.render(env.getOut()); + } + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/ShiroTags.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/ShiroTags.java new file mode 100644 index 0000000..a080997 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/ShiroTags.java @@ -0,0 +1,25 @@ +package com.ksa.shiro.freemarker; + +import freemarker.template.SimpleHash; + +/** + * Shortcut for injecting the tags into Freemarker + * + *

Usage: cfg.setSharedVeriable("shiro", new ShiroTags());

+ */ +@SuppressWarnings( "serial" ) +public class ShiroTags extends SimpleHash { + public ShiroTags() { + put("authenticated", new AuthenticatedTag()); + put("guest", new GuestTag()); + put("hasAnyPermissions", new HasAnyPermissionsTag()); + put("hasAnyRoles", new HasAnyRolesTag()); + put("hasPermission", new HasPermissionTag()); + put("hasRole", new HasRoleTag()); + put("lacksPermission", new LacksPermissionTag()); + put("lacksRole", new LacksRoleTag()); + put("notAuthenticated", new NotAuthenticatedTag()); + put("principal", new PrincipalTag()); + put("user", new UserTag()); + } +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/UserTag.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/UserTag.java new file mode 100644 index 0000000..dceba57 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/freemarker/UserTag.java @@ -0,0 +1,38 @@ +package com.ksa.shiro.freemarker; + +import freemarker.core.Environment; +import freemarker.log.Logger; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateException; + +import java.io.IOException; +import java.util.Map; + +/** + * Freemarker tag that renders the tag body if the current user known to the system, either from a successful login attempt + * (not necessarily during the current session) or from 'RememberMe' services. + * + *

Note: This is less restrictive than the AuthenticatedTag since it only assumes + * the user is who they say they are, either via a current session login or via Remember Me services, which + * makes no guarantee the user is who they say they are. The AuthenticatedTag however + * guarantees that the current user has logged in during their current session, proving they really are + * who they say they are. + * + *

The logically opposite tag of this one is the {@link org.apache.shiro.web.tags.GuestTag}. + * + *

Equivalent to {@link org.apache.shiro.web.tags.UserTag}

+ */ +public class UserTag extends SecureTag { + static final Logger log = Logger.getLogger("UserTag"); + + @SuppressWarnings( "rawtypes" ) + @Override + public void render(Environment env, Map params, TemplateDirectiveBody body) throws IOException, TemplateException { + if (getSubject() != null && getSubject().getPrincipal() != null) { + log.debug("Subject has known identity (aka 'principal'). Tag body will be evaluated."); + renderBody(env, body); + } else { + log.debug("Subject does not exist or have a known identity (aka 'principal'). Tag body will not be evaluated."); + } + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/web/tags/HasAnyPermissions.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/web/tags/HasAnyPermissions.java new file mode 100644 index 0000000..22e04b4 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/shiro/web/tags/HasAnyPermissions.java @@ -0,0 +1,31 @@ +package com.ksa.shiro.web.tags; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.tags.PermissionTag; + +public class HasAnyPermissions extends PermissionTag { + + private static final long serialVersionUID = -8914021782643585116L; + + // Delimeter that separates role names in tag attribute + private static final String PERMISSION_NAMES_DELIMETER = ","; + + protected boolean showTagBody( String permissions ) { + boolean hasAnyPermission = false; + + Subject subject = getSubject(); + + if( subject != null ) { + // Iterate through roles and check to see if the user has one of the roles + for( String p : permissions.split( PERMISSION_NAMES_DELIMETER ) ) { + if( subject.isPermitted( p.trim() ) ) { + hasAnyPermission = true; + break; + } + } + } + + return hasAnyPermission; + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupSchedule.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupSchedule.java new file mode 100644 index 0000000..cca7df1 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupSchedule.java @@ -0,0 +1,105 @@ +package com.ksa.system.backup; + +import java.util.Calendar; +import java.util.Date; +import java.util.Timer; + +import javax.servlet.ServletContext; + +import com.ksa.context.web.RuntimeConfiguration; + + +public class BackupSchedule { + + private static BackupSchedule instance; + public static BackupSchedule getInstance( ServletContext sc ) { + if( instance == null ) { + instance = new BackupSchedule( sc ); + } + return instance; + } + + public static String KEY_BACKUP_AUTO_ON = "backup.auto.on"; + public static String KEY_BACKUP_AUTO_NEXT = "backup.auto.next"; + public static String KEY_BACKUP_AUTO_INTERVAL = "backup.auto.interval"; + + public static boolean DEFAULT_BACKUP_AUTO_ON = false; + public static long DEFAULT_BACKUP_AUTO_NEXT = 0; + public static int DEFAULT_BACKUP_AUTO_INTERVAL = 7; + + private Timer timer = new Timer(); + private ServletContext context; + + public Timer getTimer() { + return timer; + } + + public void cancel() { + if( timer != null ) { + timer.cancel(); + timer = null; + } + } + + public void schedule() { + cancel(); + timer = new Timer(); + timer.scheduleAtFixedRate( new BackupTask( this, context ), getNextExecuteDate(), ( (long)getIntervalDay() ) * 24L * 60L * 60L * 1000L ); + } + + protected BackupSchedule( ServletContext sc) { + this.context = sc; + } + + public boolean isAutoBackupOn() { + return RuntimeConfiguration.getConfiguration().getBoolean( KEY_BACKUP_AUTO_ON, DEFAULT_BACKUP_AUTO_ON ); + } + + public void setAutoBackupOn( boolean on ) { + RuntimeConfiguration.getConfiguration().setProperty( KEY_BACKUP_AUTO_ON, on ); + } + + public void setNextExecuteDate( Date next ) { + long nextTicks = next.getTime(); + if( nextTicks < new Date().getTime() ) { + nextTicks = getTommorrowTicks(); + } + RuntimeConfiguration.getConfiguration().setProperty( KEY_BACKUP_AUTO_NEXT, nextTicks ); + } + + public Date getNextExecuteDate() { + Date now = new Date(); + long next = RuntimeConfiguration.getConfiguration().getLong( KEY_BACKUP_AUTO_NEXT, DEFAULT_BACKUP_AUTO_NEXT ); + if( next <= 0L ) { + next = getTommorrowTicks(); + } + long nowTime = now.getTime(); + long interval = (long) getIntervalDay(); + while( next < nowTime ) { + next += interval * 24L * 60L * 60L * 1000L; + } + return new Date( next ); + } + + protected long getTommorrowTicks() { + Calendar c = Calendar.getInstance(); + c.setTime( new Date() ); + c.add( Calendar.DATE, 1 ); + c.set( Calendar.HOUR, 0 ); + c.set( Calendar.MINUTE, 1 ); + c.set( Calendar.SECOND, 0 ); + return c.getTimeInMillis(); + } + + public void setIntervalDay( int interval ) { + RuntimeConfiguration.getConfiguration().setProperty( KEY_BACKUP_AUTO_INTERVAL, interval <= 0 ? DEFAULT_BACKUP_AUTO_INTERVAL : interval ); + } + + public int getIntervalDay() { + return RuntimeConfiguration.getConfiguration().getInt( KEY_BACKUP_AUTO_INTERVAL, DEFAULT_BACKUP_AUTO_INTERVAL ); + } + + public void save() { + RuntimeConfiguration.save(); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupTask.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupTask.java new file mode 100644 index 0000000..503a682 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/system/backup/BackupTask.java @@ -0,0 +1,107 @@ +package com.ksa.system.backup; + +import java.io.File; +import java.util.Date; +import java.util.TimerTask; + +import javax.servlet.ServletContext; + +import org.apache.commons.dbcp.BasicDataSource; +import org.apache.commons.lang.time.DateFormatUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.context.web.RuntimeConfiguration; + + +public class BackupTask extends TimerTask { + + private static final String MYSQL_PROTOCAL = "jdbc:mysql://"; + private static final String DEFAULT_BACKUP_PATH = "D:/KSA-BACKUP"; + + private static Logger logger = LoggerFactory.getLogger( BackupTask.class ); + + protected BackupSchedule schedule; + protected ServletContext context; // 废弃备份到 ServletContext 下 + + + public BackupTask( ServletContext sc ) { + this.context = sc; + } + + public BackupTask( BackupSchedule schedule, ServletContext sc ) { + this.context = sc; + this.schedule = schedule; + } + + public static String generateBackupFilename() { + return "ksa-" + DateFormatUtils.format( new Date(), "yyyyMMddHHmmss" ) + ".bak"; + } + + public static File getBackupDirectory() { + String backupPath = RuntimeConfiguration.getConfiguration().getString( "backup.path", DEFAULT_BACKUP_PATH ); + File dir = new File(backupPath); + if(!dir.exists()) { + dir.mkdir(); + } + return dir; + } + + public int doBackup() { + try { + BasicDataSource db = ServiceContextUtils.getService( BasicDataSource.class ); + + // 解析数据库链接字符串 + String url = db.getUrl().toLowerCase(); + if( !url.startsWith( MYSQL_PROTOCAL ) ) { + throw new RuntimeException( "数据备份失败,目前数据备份操作仅支持 Mysql 数据库。" ); + } + + url = url.substring( MYSQL_PROTOCAL.length() ); + String[] hostAndDb = url.split( "/" ); + // FIXME 这里没有检查url的合法性,直接使用了 + StringBuilder sb = new StringBuilder(); + String backupFilename = generateBackupFilename(); + sb.append( "mysqldump -h" ).append( hostAndDb[0] ) // 一定要加 -h localhost(或是服务器IP地址) + .append( " -u" ).append( db.getUsername() ) + .append( " -p" ).append( db.getPassword() ) + .append( " " ).append( hostAndDb[1] ) + .append( " -r " ).append( new File( getBackupDirectory(), backupFilename ).getCanonicalPath() ); + String commandStr = sb.toString(); + + // 查看数据库备份日志 + logger.warn( "执行数据库备份:" + commandStr ); + + // 执行备份操作 + Runtime rt = Runtime.getRuntime(); + Process process = rt.exec( commandStr ); + + return process.waitFor(); + + } catch( Exception e ) { + throw new RuntimeException( "数据库自动备份发生异常。", e ); + } + } + + @Override + public void run() { + Date now = new Date(); + try { + if( 0 == doBackup() ) { + logger.warn( "数据库自动备份成功。" ); + } else { + logger.error("数据库自动备份失败。"); + } + + if( this.schedule != null ) { + long interval = ( (long)schedule.getIntervalDay() ) * 24L * 60L * 60L * 1000L; + schedule.setNextExecuteDate( new Date( now.getTime() + interval ) ); + schedule.save(); + } + } catch( Throwable e ) { + logger.error( "数据库自动备份发生异常。", e ); + } + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/servlet/BackupScheduleListener.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/servlet/BackupScheduleListener.java new file mode 100644 index 0000000..2b23e04 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/servlet/BackupScheduleListener.java @@ -0,0 +1,29 @@ +package com.ksa.web.servlet; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import com.ksa.context.web.SpringServiceContextListener; +import com.ksa.system.backup.BackupSchedule; + + +public class BackupScheduleListener extends SpringServiceContextListener implements ServletContextListener { + + @Override + public void contextInitialized( ServletContextEvent sce ) { + super.contextInitialized( sce ); + BackupSchedule schedule = BackupSchedule.getInstance( sce.getServletContext() ); + if( schedule.isAutoBackupOn() ) { + // 启动自动备份 + schedule.schedule(); + } + + } + + @Override + public void contextDestroyed( ServletContextEvent sce ) { + BackupSchedule.getInstance( sce.getServletContext() ).cancel(); + super.contextDestroyed( sce ); + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/DefaultActionSupport.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/DefaultActionSupport.java new file mode 100644 index 0000000..9b1a11b --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/DefaultActionSupport.java @@ -0,0 +1,37 @@ +package com.ksa.web.struts2.action; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.opensymphony.xwork2.ActionSupport; + +public class DefaultActionSupport extends ActionSupport { + + private static final long serialVersionUID = 4414474717653162942L; + private static final Logger logger = LoggerFactory.getLogger( DefaultActionSupport.class ); + + public static final String NO_PERMISSION = "no-permission"; + + // TODO 完成自定义的 ActionSupport + + @Override + public String execute() throws Exception { + try { + return doExecute(); + } catch( RuntimeException e ) { + logger.warn( "请注意,操作发生异常!", e ); + this.addActionError( e.getMessage() ); + return ERROR; + } + } + + /** + * 具体的操作执行逻辑,无需考虑 RuntimeException 的处理。 + * @return + * @throws Exception + */ + protected String doExecute() throws Exception { + return SUCCESS; + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/JsonAction.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/JsonAction.java new file mode 100644 index 0000000..bed3d2e --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/JsonAction.java @@ -0,0 +1,12 @@ +package com.ksa.web.struts2.action; + +import com.opensymphony.xwork2.Action; + +public interface JsonAction extends Action { + + /** + * 返回需要序列化为 json 格式的数据对象。 + * @return 结果数据 + */ + Object getJsonResult(); +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/ComboDataAction.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/ComboDataAction.java new file mode 100644 index 0000000..63b25ad --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/ComboDataAction.java @@ -0,0 +1,12 @@ +package com.ksa.web.struts2.action.data; + +import com.opensymphony.xwork2.Action; + + +public interface ComboDataAction extends Action { + /** + * 获取分页数据结果。 + * @return 列表数据 + */ + Object[] getComboData(); +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DataActionSupport.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DataActionSupport.java new file mode 100644 index 0000000..b77d9e4 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DataActionSupport.java @@ -0,0 +1,32 @@ +package com.ksa.web.struts2.action.data; + +import java.lang.reflect.Array; +import java.util.Map; +import java.util.Map.Entry; + +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ActionContext; + +public class DataActionSupport extends DefaultActionSupport { + + private static final long serialVersionUID = -2812109970898284160L; + + protected Map getParameters() { + ActionContext context = ActionContext.getContext(); + Map paras = context.getParameters(); + + // 格式化参数 + for( Entry entry : paras.entrySet() ) { + Object value = entry.getValue(); + if( value.getClass().isArray() ) { + entry.setValue( Array.get( value, 0 ) ); + } + } + + return paras; + } + + protected String getActionName() { + return ActionContext.getContext().getName(); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultComboDataAction.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultComboDataAction.java new file mode 100644 index 0000000..0a0f1e0 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultComboDataAction.java @@ -0,0 +1,43 @@ +package com.ksa.web.struts2.action.data; +import java.util.List; + +import org.apache.ibatis.session.SqlSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; + +// FIXME 改进与 mybatis dao 的强耦合 +public class DefaultComboDataAction extends DataActionSupport implements ComboDataAction { + + private static final long serialVersionUID = 2776771313956095764L; + + private static final Logger log = LoggerFactory.getLogger( DefaultComboDataAction.class ); + + private static final String QUERY_DATA_STATEMENT_PREFIX = "combo-"; + + protected Object[] comboDataArray = new Object[0]; + + @Override + public String execute() throws Exception { + SqlSession sqlSession = ServiceContextUtils.getService( "sqlSession", SqlSession.class ); + if( sqlSession != null ) { + String statement = getActionName(); + try { + List gridDataList = sqlSession.selectList( QUERY_DATA_STATEMENT_PREFIX + statement, getParameters() ); + if( gridDataList != null && !gridDataList.isEmpty() ) { + comboDataArray = gridDataList.toArray(); + } + } catch( Throwable e ) { + log.warn( "列表数据获取失败,ActionName:" + statement, e ); + } + } + return SUCCESS; + } + + @Override + public Object[] getComboData() { + return comboDataArray; + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultGridDataAction.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultGridDataAction.java new file mode 100644 index 0000000..95a3613 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/DefaultGridDataAction.java @@ -0,0 +1,63 @@ +package com.ksa.web.struts2.action.data; +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.session.SqlSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.dao.mybatis.session.RowBounds; +import com.ksa.util.StringUtils; + +// FIXME 改进与 mybatis dao 的强耦合 +public class DefaultGridDataAction extends GridDataActionSupport { + + private static final long serialVersionUID = -3569929952546726658L; + + private static final Logger log = LoggerFactory.getLogger( DefaultGridDataAction.class ); + + private static final String QUERY_DATA_STATEMENT_PREFIX = "grid-"; + private static final String QUERY_COUNT_STATEMENT_PREFIX= "count-"; + + protected Object[] gridDataArray = new Object[0]; + protected int gridDataCount = 0; + + @Override + public String execute() throws Exception { + SqlSession sqlSession = ServiceContextUtils.getService( SqlSession.class ); + if( sqlSession != null ) { + String statement = getActionName(); + Map paras = getParameters(); + if( StringUtils.hasText( this.sort ) ) { + paras.put( "_sort", this.sort ); + paras.put( "_order", this.order ); + } + + try { + List gridDataList = sqlSession.selectList( QUERY_DATA_STATEMENT_PREFIX + statement, paras, new RowBounds( this.page, this.rows ) ); + if( gridDataList != null && !gridDataList.isEmpty() ) { + gridDataArray = gridDataList.toArray(); + Integer count = (Integer) sqlSession.selectOne( QUERY_COUNT_STATEMENT_PREFIX + statement, paras ); + if( count != null ) { + gridDataCount = count.intValue(); + } + } + } catch( Throwable e ) { + log.warn( "表格数据获取失败,ActionName:" + statement, e ); + } + } + return SUCCESS; + } + + @Override + protected Object[] queryGridData() { + return gridDataArray; + } + + @Override + protected int queryGridDataCount() { + return gridDataCount; + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataAction.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataAction.java new file mode 100644 index 0000000..8895471 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataAction.java @@ -0,0 +1,44 @@ +package com.ksa.web.struts2.action.data; + +import com.ksa.web.struts2.action.model.GridDataModel; +import com.opensymphony.xwork2.Action; + +public interface GridDataAction extends Action { + + /** 降序排序 */ + public static final String SORT_ORDER_DESC = "desc"; + + /** 升序排序 */ + public static final String SORT_ORDER_ASC = "asc"; + + /** + * 设置所获取的分页数据的页码,起始页码为 1 。 + * @param page 页码,起始页码为 1 + */ + void setPage( int page ); + + /** + * 设置所获取的分页数据的单页数据量。 + * @param rows 单页数据量 + */ + void setRows( int rows ); + + /** + * 设置所获取的分页数据的排序列名称。 + * @param sort 排序列名称 + */ + void setSort( String sort ); + + /** + * 设置所获取的分页数据的排序顺序,'asc' 表示顺序,'desc' 表示逆序。 + * @param order 'asc' 表示顺序,'desc' 表示逆序 + */ + void setOrder( String order ); + + /** + * 获取分页数据结果。 + * @return 列表数据 + */ + GridDataModel getGridData(); + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataActionSupport.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataActionSupport.java new file mode 100644 index 0000000..63022c6 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/data/GridDataActionSupport.java @@ -0,0 +1,70 @@ +package com.ksa.web.struts2.action.data; + +import com.ksa.web.struts2.action.model.GridDataModel; + +public abstract class GridDataActionSupport extends DataActionSupport implements GridDataAction { + + private static final long serialVersionUID = -894387782806926084L; + + /** 页码,起始页码为 1。 */ + protected int page = 1; + /** 单页数据量 - 默认不限制单页数量。 */ + protected int rows = Integer.MAX_VALUE; + /** 排序列名称 */ + protected String sort; + /** 'asc' 表示顺序,'desc' 表示逆序 。*/ + protected String order = SORT_ORDER_ASC; + + @Override + public void setPage( int page ) { + if( page >= 1 ) { + this.page = page; + } + } + + @Override + public void setRows( int rows ) { + if( rows > 0 ) { + this.rows = rows; + } + } + + @Override + public void setSort( String sort ) { + this.sort = sort; + } + + @Override + public void setOrder( String order ) { + if( SORT_ORDER_DESC.equalsIgnoreCase( order ) ) { + this.order = SORT_ORDER_DESC; + } + } + + /** + * 获取列表数据结果。 + * + * @return 结果数据数组 + */ + protected abstract Object[] queryGridData(); + + /** + * 获取所有数据的数量。 + * + * @return 数据量 + */ + protected abstract int queryGridDataCount(); + + @Override + public GridDataModel getGridData() { + Object[] rows = queryGridData(); + if( rows == null ) { + rows = new Object[0]; + } + int total = queryGridDataCount(); + if( total < rows.length ) { + total = rows.length; + } + return new GridDataModel( total, rows ); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/GridDataModel.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/GridDataModel.java new file mode 100644 index 0000000..197a319 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/GridDataModel.java @@ -0,0 +1,62 @@ +package com.ksa.web.struts2.action.model; + +import java.io.Serializable; + +/** + * 表格数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class GridDataModel implements Serializable { + + private static final long serialVersionUID = -207550381178775037L; + + private Object[] rows; + private Object[] footer; + private int total; + + public GridDataModel( Object[] rows ) { + this( rows.length, rows ); + } + + public GridDataModel( Object[] rows, Object[] footer ) { + this( rows.length, rows, footer ); + } + + public GridDataModel(int total, Object[] rows) { + this( total, rows, null ); + } + + public GridDataModel( int total, Object[] rows, Object[] footer ) { + this.total = total; + this.rows = rows; + this.footer = footer; + } + + public int getTotal() { + return total; + } + + + public void setTotal( int total ) { + this.total = total; + } + + public Object[] getRows() { + return rows; + } + + public void setRows( Object[] rows ) { + this.rows = rows; + } + + public Object[] getFooter() { + return footer; + } + + public void setFooter( Object[] footer ) { + this.footer = footer; + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/JsonResult.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/JsonResult.java new file mode 100644 index 0000000..0f873b5 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/action/model/JsonResult.java @@ -0,0 +1,64 @@ +package com.ksa.web.struts2.action.model; + +import java.io.Serializable; + +/** + * Json 数据模型。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class JsonResult implements Serializable { + + private static final long serialVersionUID = -1773008502579676366L; + + private String status; + + private String message; + + private Object data; + + public JsonResult() { + this( "unknow", "", null ); + } + + public JsonResult( String status ) { + this( status, "", null ); + } + + public JsonResult( String status, String message ) { + this( status, message, null ); + } + + public JsonResult( String status, String message, Object data ) { + this.status = status; + this.message = message; + this.data = data; + } + + public String getStatus() { + return status; + } + + public void setStatus( String status ) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage( String message ) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData( Object data ) { + this.data = data; + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/interceptor/DataInitializedInterceptor.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/interceptor/DataInitializedInterceptor.java new file mode 100644 index 0000000..0c8b4a1 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/interceptor/DataInitializedInterceptor.java @@ -0,0 +1,33 @@ +package com.ksa.web.struts2.interceptor; + +import com.ksa.context.web.RuntimeConfiguration; +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; + +/** + * 判断是否执行过数据迁移的拦截器。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class DataInitializedInterceptor extends AbstractInterceptor { + + private static final long serialVersionUID = 274352543038390892L; + + private static String RESULT_TO_INITIALIZE = "to-initialize"; + + @Override + public String intercept( ActionInvocation invocation ) throws Exception { + if( isInitialized() ) { + return invocation.invoke(); + } else { + return RESULT_TO_INITIALIZE; + } + } + + private boolean isInitialized() { + return RuntimeConfiguration.getConfiguration().getBoolean( "initialized", false ); + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/FreemarkerStreamResult.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/FreemarkerStreamResult.java new file mode 100644 index 0000000..3e89af0 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/FreemarkerStreamResult.java @@ -0,0 +1,99 @@ +package com.ksa.web.struts2.views.freemarker; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.views.freemarker.FreemarkerResult; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.util.ValueStack; + +import freemarker.template.Template; +import freemarker.template.TemplateModel; + +/** + * FreeMarkerStreamResult 结合了 StreamResult 和 FreemarkerResult,将通过 Freemarker 生成的下载文件发送到客户端。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class FreemarkerStreamResult extends FreemarkerResult { + + private static final long serialVersionUID = -7160610996561043631L; + + protected String contentDisposition = "inline"; + protected boolean allowCaching = true; + + @Override + protected boolean preTemplateProcess( Template template, TemplateModel model ) throws IOException { + // Override any parameters using values on the stack + resolveParamsFromStack(invocation.getStack(), invocation); + + // Find the Response in context + HttpServletResponse oResponse = ServletActionContext.getResponse(); + + // Set the content-disposition + if (contentDisposition != null) { + oResponse.addHeader("Content-Disposition", conditionalParse(contentDisposition, invocation)); + } + + // Set the cache control headers if neccessary + if (!allowCaching) { + oResponse.addHeader("Pragma", "no-cache"); + oResponse.addHeader("Cache-Control", "no-cache"); + } + + return super.preTemplateProcess( template, model ); + } + + /** + * Tries to lookup the parameters on the stack. Will override any existing parameters + * + * @param stack The current value stack + */ + protected void resolveParamsFromStack(ValueStack stack, ActionInvocation invocation) { + String disposition = stack.findString("contentDisposition"); + if (disposition != null) { + setContentDisposition(disposition); + } + + String contentType = stack.findString("contentType"); + if (contentType != null) { + setContentType(contentType); + } + } + + /** + * @return Returns the whether or not the client should be requested to allow caching of the data stream. + */ + public boolean getAllowCaching() { + return allowCaching; + } + + /** + * Set allowCaching to false to indicate that the client should be requested not to cache the data stream. + * This is set to false by default + * + * @param allowCaching Enable caching. + */ + public void setAllowCaching(boolean allowCaching) { + this.allowCaching = allowCaching; + } + + /** + * @return Returns the Content-disposition header value. + */ + public String getContentDisposition() { + return contentDisposition; + } + + /** + * @param contentDisposition the Content-disposition header value to use. + */ + public void setContentDisposition(String contentDisposition) { + this.contentDisposition = contentDisposition; + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/ShiroFreemarkerManager.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/ShiroFreemarkerManager.java new file mode 100644 index 0000000..3046861 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/struts2/views/freemarker/ShiroFreemarkerManager.java @@ -0,0 +1,25 @@ +package com.ksa.web.struts2.views.freemarker; + +import javax.servlet.ServletContext; + +import org.apache.struts2.views.freemarker.FreemarkerManager; + +import com.ksa.shiro.freemarker.ShiroTags; + +import freemarker.template.Configuration; +import freemarker.template.TemplateException; + + +public class ShiroFreemarkerManager extends FreemarkerManager { + + @Override + protected Configuration createConfiguration( ServletContext servletContext ) throws TemplateException { + Configuration cfg = super.createConfiguration( servletContext ); + cfg.setSharedVariable("shiro", new ShiroTags()); + return cfg; + } + + public void forTest()throws TemplateException{ + createConfiguration(null); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/HttpServletRequestWrapper.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/HttpServletRequestWrapper.java new file mode 100644 index 0000000..a192d47 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/HttpServletRequestWrapper.java @@ -0,0 +1,318 @@ +package com.ksa.web.wro4j; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +/** + * 解决 jsessionid 产生的问题。 + * + * @author 麻文强 + * + * @since + */ +public class HttpServletRequestWrapper implements HttpServletRequest { + + protected HttpServletRequest request; + + public HttpServletRequestWrapper( HttpServletRequest request ) { + this.request = request; + } + + @Override + public Object getAttribute( String name ) { + return request.getAttribute( name ); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public Enumeration getAttributeNames() { + return request.getAttributeNames(); + } + + @Override + public String getCharacterEncoding() { + return request.getCharacterEncoding(); + } + + @Override + public void setCharacterEncoding( String env ) throws UnsupportedEncodingException { + request.setCharacterEncoding( env ); + } + + @Override + public int getContentLength() { + return request.getContentLength(); + } + + @Override + public String getContentType() { + return request.getContentType(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return request.getInputStream(); + } + + @Override + public String getParameter( String name ) { + return request.getParameter( name ); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public Enumeration getParameterNames() { + return request.getParameterNames(); + } + + @Override + public String[] getParameterValues( String name ) { + return request.getParameterValues( name ); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public Map getParameterMap() { + return request.getParameterMap(); + } + + @Override + public String getProtocol() { + return request.getProtocol(); + } + + @Override + public String getScheme() { + return request.getScheme(); + } + + @Override + public String getServerName() { + return request.getServerName(); + } + + @Override + public int getServerPort() { + return request.getServerPort(); + } + + @Override + public BufferedReader getReader() throws IOException { + return request.getReader(); + } + + @Override + public String getRemoteAddr() { + return request.getRemoteAddr(); + } + + @Override + public String getRemoteHost() { + return request.getRemoteHost(); + } + + @Override + public void setAttribute( String name, Object o ) { + request.setAttribute( name, o ); + } + + @Override + public void removeAttribute( String name ) { + request.removeAttribute( name ); + } + + @Override + public Locale getLocale() { + return request.getLocale(); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public Enumeration getLocales() { + return request.getLocales(); + } + + @Override + public boolean isSecure() { + return request.isSecure(); + } + + @Override + public RequestDispatcher getRequestDispatcher( String path ) { + return request.getRequestDispatcher( path ); + } + + @SuppressWarnings( "deprecation" ) + @Override + public String getRealPath( String path ) { + return request.getRealPath( path ); + } + + @Override + public int getRemotePort() { + return request.getRemotePort(); + } + + @Override + public String getLocalName() { + return request.getLocalName(); + } + + @Override + public String getLocalAddr() { + return request.getLocalAddr(); + } + + @Override + public int getLocalPort() { + return request.getLocalPort(); + } + + @Override + public String getAuthType() { + return request.getAuthType(); + } + + @Override + public Cookie[] getCookies() { + return request.getCookies(); + } + + @Override + public long getDateHeader( String name ) { + return request.getDateHeader( name ); + } + + @Override + public String getHeader( String name ) { + return request.getHeader( name ); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public Enumeration getHeaders( String name ) { + return request.getHeaders( name ); + } + + @SuppressWarnings( "rawtypes" ) + @Override + public Enumeration getHeaderNames() { + return request.getHeaderNames(); + } + + @Override + public int getIntHeader( String name ) { + return request.getIntHeader( name ); + } + + @Override + public String getMethod() { + return request.getMethod(); + } + + @Override + public String getPathInfo() { + return request.getPathInfo(); + } + + @Override + public String getPathTranslated() { + return request.getPathTranslated(); + } + + @Override + public String getContextPath() { + return request.getContextPath(); + } + + @Override + public String getQueryString() { + return request.getQueryString(); + } + + @Override + public String getRemoteUser() { + return request.getRemoteUser(); + } + + @Override + public boolean isUserInRole( String role ) { + return request.isUserInRole( role ); + } + + @Override + public Principal getUserPrincipal() { + return request.getUserPrincipal(); + } + + @Override + public String getRequestedSessionId() { + return request.getRequestedSessionId(); + } + + @Override + public String getRequestURI() { + // 解决 JSession 的问题 + String uri = request.getRequestURI(); + if( uri != null ) { + int index = uri.toLowerCase().indexOf( ";jsessionid" ); + if( index >= 0 ) { + uri = uri.substring( 0, index ); + } + } + return uri; + } + + @Override + public StringBuffer getRequestURL() { + return request.getRequestURL(); + } + + @Override + public String getServletPath() { + return request.getServletPath(); + } + + @Override + public HttpSession getSession( boolean create ) { + return request.getSession( create ); + } + + @Override + public HttpSession getSession() { + return request.getSession(); + } + + @Override + public boolean isRequestedSessionIdValid() { + return request.isRequestedSessionIdFromCookie(); + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return request.isRequestedSessionIdFromCookie(); + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return request.isRequestedSessionIdFromURL(); + } + + @SuppressWarnings( "deprecation" ) + @Override + public boolean isRequestedSessionIdFromUrl() { + return request.isRequestedSessionIdFromUrl(); + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/MultiXmlWroModelFactory.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/MultiXmlWroModelFactory.java new file mode 100644 index 0000000..5716db5 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/MultiXmlWroModelFactory.java @@ -0,0 +1,82 @@ +package com.ksa.web.wro4j; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ro.isdc.wro.model.WroModel; +import ro.isdc.wro.model.factory.WroModelFactory; +import ro.isdc.wro.model.factory.XmlModelFactory; + +/** + * WroModelFactory 的具体实现。基于多个 xml 文件来创建 WroModel 对象,每个 xml 配置文件均配置了各自的 groups。 + *

+ * WroModelFactory 创建 WroModel 所需的多个 xml 配置文件是从 WroModelConfigurationCache 中获取的。 也就是说在创建 WroModel 前,所有配置文件应该均已获取并存放在 + * WroModelConfigurationCache 中。 + *

+ * 注意:目前的解析方式无法解读 xml 配置之间的依赖关系,因而所提供的 xml 配置文件应该是项目独立的。 + * + * @author 麻文强 + * + * @since v0.0.1 + * + * @see com.bjsasc.probe.extension.wro4j.WroModelConfigurationCache + */ +public class MultiXmlWroModelFactory implements WroModelFactory { + + private static final Logger log = LoggerFactory.getLogger( MultiXmlWroModelFactory.class ); + + private String configurationCacheKey; + + /** + * 使用 WroModelConfigurationCache 默认实例来获取 WroModel 配置文件并解析。 + */ + public MultiXmlWroModelFactory() { + this( WroModelConfigurationCache.DEFAULT_CACHE_KEY ); + } + + /** + * 使用指定的 WroModelConfigurationCache 实例来获取 WroModel 配置文件并解析。 + * + * @param configurationCacheKey + * 存放配置文件的 WroModelConfigurationCache 实例名称。 + */ + public MultiXmlWroModelFactory( String configurationCacheKey ) { + this.configurationCacheKey = configurationCacheKey; + } + + @Override + public WroModel create() { + WroModel finalModel = new WroModel(); + Collection configs = WroModelConfigurationCache.get( configurationCacheKey ).getConfigurations(); + for( URL url : configs ) { + try { + WroModel model = getParseProxy( url ).create(); + finalModel.merge( model ); + } catch( Throwable e ) { + log.warn( "Fail to parse wro4j configuration '" + url + "'.", e ); + } + log.debug( "Parsed wro4j configuration '{}'.", url ); + } + return finalModel; + } + + @Override + public void destroy() { + + } + + private XmlModelFactory getParseProxy( final URL wroXMLUrl ) { + return new XmlModelFactory() { + + @Override + protected InputStream getModelResourceAsStream() throws IOException { + return wroXMLUrl.openStream(); + } + }; + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroFilter.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroFilter.java new file mode 100644 index 0000000..bf1dd13 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroFilter.java @@ -0,0 +1,471 @@ +package com.ksa.web.wro4j; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.net.URL; +import java.util.Calendar; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.time.FastDateFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; + +import com.ksa.util.ResourceUtils; +import com.ksa.util.StringUtils; + +import ro.isdc.wro.WroRuntimeException; +import ro.isdc.wro.config.Context; +import ro.isdc.wro.config.WroConfigurationChangeListener; +import ro.isdc.wro.config.factory.PropertiesAndFilterConfigWroConfigurationFactory; +import ro.isdc.wro.config.jmx.WroConfiguration; +import ro.isdc.wro.http.HttpHeader; +import ro.isdc.wro.manager.CacheChangeCallbackAware; +import ro.isdc.wro.manager.WroManagerFactory; +import ro.isdc.wro.manager.factory.ConfigurableWroManagerFactory; +import ro.isdc.wro.model.factory.WroModelFactory; +import ro.isdc.wro.model.group.DefaultGroupExtractor; +import ro.isdc.wro.model.group.GroupExtractor; +import ro.isdc.wro.model.resource.ResourceType; +import ro.isdc.wro.model.resource.processor.factory.ConfigurableProcessorsFactory; +import ro.isdc.wro.util.ObjectFactory; +import ro.isdc.wro.util.WroUtil; + + +public class WroFilter implements Filter { + private static final Logger log = LoggerFactory.getLogger( WroFilter.class ); + + // 设置 headers 时,必须返回英文,所以 Locale 要设置为 English + private static final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance( "E, dd MMM yyyy HH:mm:ss z", + TimeZone.getTimeZone( "GMT" ), Locale.ENGLISH ); + + public static final String DEFAULT_WRO_CONFIG = "/WEB-INF/wro.xml"; + + public static final String DEFAULT_WRO_CONFIG_PATTERN = "classpath*:wro.xml"; + + public static final String DEFAULT_PRE_PROCESSORS = "cssImport,cssUrlRewriting,semicolonAppender"; + + public static final String DEFAULT_POST_PROCESSORS = "cssMinJawr,jsMin"; + + /** + * Default value used by Cache-control header. + */ + private static final String DEFAULT_CACHE_CONTROL_VALUE = "public, max-age=315360000"; + + /** + * wro API mapping path. If request uri contains this, exposed API method will be invoked. + */ + public static final String PATH_API = "wroAPI"; + + /** + * API - reload cache method call + */ + public static final String API_RELOAD_CACHE = PATH_API + "/reloadCache"; + + /** + * API - reload model method call + */ + public static final String API_RELOAD_MODEL = PATH_API + "/reloadModel"; + + /** + * Filter config. + */ + protected FilterConfig filterConfig; + + /** + * Wro configuration. + */ + protected WroConfiguration wroConfiguration; + + /** + * WroManagerFactory. The brain of the optimizer. + */ + protected WroManagerFactory wroManagerFactory; + + /** + * Map containing header values used to control caching. The keys from this values are trimmed and lower-cased when put, in + * order to avoid duplicate keys. This is done, because according to RFC 2616 Message Headers field names are + * case-insensitive. + */ + @SuppressWarnings( "serial" ) + protected final Map headersMap = new LinkedHashMap() { + + @Override + public String put( final String key, final String value ) { + return super.put( key.trim().toLowerCase(), value ); + } + + @Override + public String get( final Object key ) { + return super.get( ( (String)key ).toLowerCase() ); + } + }; + + /** + * {@inheritDoc} + */ + public final void init( final FilterConfig config ) + throws ServletException { + this.filterConfig = config; + wroConfiguration = newWroConfigurationFactory().create(); + initWroManagerFactory(); + initHeaderValues(); + registerChangeListeners(); + doInit( config ); + } + + /** + * @return implementation of {@link WroConfigurationFactory} used to create a {@link WroConfiguration} object. + */ + protected ObjectFactory newWroConfigurationFactory() { + return new PropertiesAndFilterConfigWroConfigurationFactory( filterConfig ); + } + + /** + * Initialize {@link WroManagerFactory}. + */ + private void initWroManagerFactory() { + this.wroManagerFactory = getWroManagerFactory(); + if( wroManagerFactory instanceof CacheChangeCallbackAware ) { + // register cache change callback -> when cache is changed, update headers values. + ( (CacheChangeCallbackAware)wroManagerFactory ).registerCallback( new PropertyChangeListener() { + + public void propertyChange( final PropertyChangeEvent evt ) { + // update header values + initHeaderValues(); + } + } ); + } + } + + /** + * Register property change listeners. + */ + private void registerChangeListeners() { + wroConfiguration.registerCacheUpdatePeriodChangeListener( new PropertyChangeListener() { + + public void propertyChange( final PropertyChangeEvent event ) { + // reset cache headers when any property is changed in order to avoid browser caching + initHeaderValues(); + if( wroManagerFactory instanceof WroConfigurationChangeListener ) { + ( (WroConfigurationChangeListener)wroManagerFactory ).onCachePeriodChanged(); + } + } + } ); + wroConfiguration.registerModelUpdatePeriodChangeListener( new PropertyChangeListener() { + + public void propertyChange( final PropertyChangeEvent event ) { + initHeaderValues(); + if( wroManagerFactory instanceof WroConfigurationChangeListener ) { + ( (WroConfigurationChangeListener)wroManagerFactory ).onModelPeriodChanged(); + } + } + } ); + log.debug( "Cache & Model change listeners were registered" ); + } + + /** + * Initialize header values. + */ + private void initHeaderValues() { + // put defaults + if( !wroConfiguration.isDebug() ) { + final Long timestamp = new Date().getTime(); + final Calendar cal = Calendar.getInstance(); + cal.roll( Calendar.YEAR, 1 ); + headersMap.put( HttpHeader.CACHE_CONTROL.toString(), DEFAULT_CACHE_CONTROL_VALUE ); + headersMap.put( HttpHeader.LAST_MODIFIED.toString(), DATE_FORMAT.format( timestamp ) ); + headersMap.put( HttpHeader.EXPIRES.toString(), DATE_FORMAT.format( cal.getTimeInMillis() ) ); + } + final String headerParam = wroConfiguration.getHeader(); + if( StringUtils.hasLength( headerParam ) ) { + try { + if( headerParam.contains( "|" ) ) { + final String[] headers = headerParam.split( "[|]" ); + for( final String header : headers ) { + parseHeader( header ); + } + } else { + parseHeader( headerParam ); + } + } catch( final Exception e ) { + throw new WroRuntimeException( "Invalid header init-param value: " + headerParam + + ". A correct value should have the following format: " + + ": | : . " + "Ex: + * + * @param response + * {@link HttpServletResponse} object. + */ + protected void setResponseHeaders( final HttpServletResponse response ) { + // prevent caching when in development mode + if( wroConfiguration.isDebug() ) { + WroUtil.addNoCacheHeaders( response ); + } else { + // Force resource caching as best as possible + for( final Map.Entry entry : headersMap.entrySet() ) { + response.setHeader( entry.getKey(), entry.getValue() ); + } + } + } + + /** + * Factory method for {@link WroManagerFactory}. Override this method, in order to change the way filter use factory. + * + * @return {@link WroManagerFactory} object. + */ + protected WroManagerFactory getWroManagerFactory() { + if( !StringUtils.hasLength( wroConfiguration.getWroManagerClassName() ) ) { + // If no context param was specified we return the default factory + return newWroManagerFactory(); + } else { + // Try to find the specified factory class + Class factoryClass = null; + try { + factoryClass = Thread.currentThread().getContextClassLoader().loadClass( + wroConfiguration.getWroManagerClassName() ); + // Instantiate the factory + return (WroManagerFactory)factoryClass.newInstance(); + } catch( final Exception e ) { + throw new WroRuntimeException( "Exception while loading WroManagerFactory class", e ); + } + } + } + + protected WroManagerFactory newWroManagerFactory() { + return new ConfigurableWroManagerFactory() { + + @Override + protected GroupExtractor newGroupExtractor() { + return new DefaultGroupExtractor() { + + // 改变原有压缩的逻辑 + // 当debug == true 时,不压缩 + // 当 request 的参数 minimize=false 时,不压缩 + @Override + public boolean isMinimized( HttpServletRequest request ) { + if( request == null ) { + throw new IllegalArgumentException( "Request cannot be NULL!" ); + } + final String minimizeAsString = request.getParameter( PARAM_MINIMIZE ); + if( Context.get().getConfig().isDebug() ) { + return false; // debug 模式不压缩 + } + // request 的参数 minimize=false 时,不压缩 + return !( "false".equalsIgnoreCase( minimizeAsString ) ); + } + + @Override + public String getGroupName( HttpServletRequest request ) { + return super.getGroupName( new HttpServletRequestWrapper( request ) ); + } + + @Override + public ResourceType getResourceType( HttpServletRequest request ) { + return super.getResourceType( new HttpServletRequestWrapper( request ) ); + } + + }; + } + + @Override + protected WroModelFactory newModelFactory() { + final String KEY = "__probe_default_wro_config__"; + WroModelConfigurationCache cache = WroModelConfigurationCache.get( KEY ); + try { + final ServletContext servletContext = Context.get().getServletContext(); + //Don't allow NPE, throw a more detailed exception + if ( servletContext != null ) { + URL defaultWro = servletContext.getResource( DEFAULT_WRO_CONFIG ); + if( defaultWro != null ) { + log.debug( "Load default wro config resource '{}'.", DEFAULT_WRO_CONFIG ); + cache.add( defaultWro ); + } + } + Resource[] resources = ResourceUtils.getResources( DEFAULT_WRO_CONFIG_PATTERN ); + if( resources != null ) { + log.debug( "Load '{}' wro config resources ( '{}' ) in each bundles.", resources.length, DEFAULT_WRO_CONFIG_PATTERN ); + for( Resource resource : resources ) { + cache.add( resource.getURL() ); + } + } + return new MultiXmlWroModelFactory( KEY ); + } catch( IOException e ) { + log.warn( "Fail to load wro.xml config files.", e ); + return super.newModelFactory(); + } + } + + @Override + protected Properties newConfigProperties() { + // default location is /WEB-INF/wro.properties + final Properties props = new Properties(); + // 加入自定义的默认处理器 + props.put( ConfigurableProcessorsFactory.PARAM_PRE_PROCESSORS, DEFAULT_PRE_PROCESSORS ); + props.put( ConfigurableProcessorsFactory.PARAM_POST_PROCESSORS, DEFAULT_POST_PROCESSORS ); + try { + props.load( PropertiesAndFilterConfigWroConfigurationFactory + .defaultConfigPropertyStream( Context.get().getFilterConfig() ) ); + + } catch( final Exception e ) { + log.debug( "No configuration property file found." ); + } + return props; + } + }; + } + + /** + * @return the {@link WroConfiguration} associated with this filter instance. + */ + public final WroConfiguration getWroConfiguration() { + return this.wroConfiguration; + } + + /** + * {@inheritDoc} + */ + public void destroy() { + wroConfiguration.destroy(); + Context.destroy(); + wroManagerFactory.destroy(); + } +} diff --git a/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroModelConfigurationCache.java b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroModelConfigurationCache.java new file mode 100644 index 0000000..c607a4f --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/java/com/ksa/web/wro4j/WroModelConfigurationCache.java @@ -0,0 +1,54 @@ +package com.ksa.web.wro4j; + +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +/** + * Wro4j 配置文件缓存,缓存配置文件的 URL 地址。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class WroModelConfigurationCache { + + protected static final Map CACHE = new HashMap(); + + public static final String DEFAULT_CACHE_KEY = "com.bjsasc.probe.extension.wro4j"; + + public static WroModelConfigurationCache get( String key ) { + if( key == null ) { + key = DEFAULT_CACHE_KEY; + } + if( !CACHE.containsKey( key ) ) { + CACHE.put( key, new WroModelConfigurationCache() ); + } + return CACHE.get( key ); + } + + public static WroModelConfigurationCache get() { + return get( DEFAULT_CACHE_KEY ); + } + + private Collection configurations = new HashSet(); + + public boolean add( URL url ) { + return getConfigurations().add( url ); + } + + public boolean remove( URL url ) { + return getConfigurations().remove( url ); + } + + public boolean contains( URL url ) { + return configurations.contains( url ); + } + + public Collection getConfigurations() { + return configurations; + } + +} diff --git a/test_input/ksa/ksa-web-core/src/main/resources/META-INF/ksa-shiro.tld b/test_input/ksa/ksa-web-core/src/main/resources/META-INF/ksa-shiro.tld new file mode 100644 index 0000000..79913a4 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/resources/META-INF/ksa-shiro.tld @@ -0,0 +1,158 @@ + + + + + + 1.1.2 + 1.2 + Apache Shiro Extension + /shiro-tags + Extended Apache Shiro JSP Tag Library. + + + hasAnyPermissions + com.ksa.shiro.web.tags.HasAnyPermissions + JSP + Displays body content only if the current user has one of the specified permissions from a + comma-separated list of permissions. + + + name + true + true + + + + + hasPermission + org.apache.shiro.web.tags.HasPermissionTag + JSP + Displays body content only if the current Subject (user) + 'has' (implies) the specified permission (i.e the user has the specified ability). + + + name + true + true + + + + + lacksPermission + org.apache.shiro.web.tags.LacksPermissionTag + JSP + Displays body content only if the current Subject (user) does + NOT have (not imply) the specified permission (i.e. the user lacks the specified ability) + + + name + true + true + + + + + hasRole + org.apache.shiro.web.tags.HasRoleTag + JSP + Displays body content only if the current user has the specified role. + + name + true + true + + + + + + hasAnyRoles + org.apache.shiro.web.tags.HasAnyRolesTag + JSP + Displays body content only if the current user has one of the specified roles from a + comma-separated list of role names. + + + name + true + true + + + + + lacksRole + org.apache.shiro.web.tags.LacksRoleTag + JSP + Displays body content only if the current user does NOT have the specified role + (i.e. they explicitly lack the specified role) + + + name + true + true + + + + + authenticated + org.apache.shiro.web.tags.AuthenticatedTag + JSP + Displays body content only if the current user has successfully authenticated + _during their current session_. It is more restrictive than the 'user' tag. + It is logically opposite to the 'notAuthenticated' tag. + + + + + notAuthenticated + org.apache.shiro.web.tags.NotAuthenticatedTag + JSP + Displays body content only if the current user has NOT succesfully authenticated + _during their current session_. It is logically opposite to the 'authenticated' tag. + + + + + user + org.apache.shiro.web.tags.UserTag + JSP + Displays body content only if the current Subject has a known identity, either + from a previous login or from 'RememberMe' services. Note that this is semantically different + from the 'authenticated' tag, which is more restrictive. It is logically + opposite to the 'guest' tag. + + + + + guest + org.apache.shiro.web.tags.GuestTag + JSP + Displays body content only if the current Subject IS NOT known to the system, either + because they have not logged in or they have no corresponding 'RememberMe' identity. It is logically + opposite to the 'user' tag. + + + + + principal + org.apache.shiro.web.tags.PrincipalTag + JSP + Displays the user's principal or a property of the user's principal. + + type + false + true + + + property + false + true + + + defaultValue + false + true + + + + diff --git a/test_input/ksa/ksa-web-core/src/main/resources/struts-default.xml b/test_input/ksa/ksa-web-core/src/main/resources/struts-default.xml new file mode 100644 index 0000000..ac7d000 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/resources/struts-default.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + application/json + true + false + true + jsonResult + + + + + + + + + + + + + + + + + /template/no-permission.ftl + + data-initialize.action + + + + + + + + diff --git a/test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-combodata.xml b/test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-combodata.xml new file mode 100644 index 0000000..339a6a1 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-combodata.xml @@ -0,0 +1,23 @@ + + + + + + + + + application/json + true + false + true + comboData + + + + + + + + diff --git a/test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-griddata.xml b/test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-griddata.xml new file mode 100644 index 0000000..c071ca3 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/resources/struts2/struts-griddata.xml @@ -0,0 +1,23 @@ + + + + + + + + + application/json + true + false + true + gridData + + + + + + + + diff --git a/test_input/ksa/ksa-web-core/src/main/resources/template/exception/service-exception.ftl b/test_input/ksa/ksa-web-core/src/main/resources/template/exception/service-exception.ftl new file mode 100644 index 0000000..e49e2b7 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/resources/template/exception/service-exception.ftl @@ -0,0 +1,13 @@ + + + +${stack.findValue("exception.name")!} + + +

+ <@s.actionerror/>
+ ${stack.findValue("exception.message")!}
+ ${stack.findValue("exceptionStack")!} +
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/resources/template/no-permission.ftl b/test_input/ksa/ksa-web-core/src/main/resources/template/no-permission.ftl new file mode 100644 index 0000000..85f905c --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/resources/template/no-permission.ftl @@ -0,0 +1,11 @@ + + + +没有操作权限 + + +
+
对不起,您没有此功能的操作权限!
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-core/src/main/scripts/META-INF/ksa-shiro.tld b/test_input/ksa/ksa-web-core/src/main/scripts/META-INF/ksa-shiro.tld new file mode 100644 index 0000000..79913a4 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/scripts/META-INF/ksa-shiro.tld @@ -0,0 +1,158 @@ + + + + + + 1.1.2 + 1.2 + Apache Shiro Extension + /shiro-tags + Extended Apache Shiro JSP Tag Library. + + + hasAnyPermissions + com.ksa.shiro.web.tags.HasAnyPermissions + JSP + Displays body content only if the current user has one of the specified permissions from a + comma-separated list of permissions. + + + name + true + true + + + + + hasPermission + org.apache.shiro.web.tags.HasPermissionTag + JSP + Displays body content only if the current Subject (user) + 'has' (implies) the specified permission (i.e the user has the specified ability). + + + name + true + true + + + + + lacksPermission + org.apache.shiro.web.tags.LacksPermissionTag + JSP + Displays body content only if the current Subject (user) does + NOT have (not imply) the specified permission (i.e. the user lacks the specified ability) + + + name + true + true + + + + + hasRole + org.apache.shiro.web.tags.HasRoleTag + JSP + Displays body content only if the current user has the specified role. + + name + true + true + + + + + + hasAnyRoles + org.apache.shiro.web.tags.HasAnyRolesTag + JSP + Displays body content only if the current user has one of the specified roles from a + comma-separated list of role names. + + + name + true + true + + + + + lacksRole + org.apache.shiro.web.tags.LacksRoleTag + JSP + Displays body content only if the current user does NOT have the specified role + (i.e. they explicitly lack the specified role) + + + name + true + true + + + + + authenticated + org.apache.shiro.web.tags.AuthenticatedTag + JSP + Displays body content only if the current user has successfully authenticated + _during their current session_. It is more restrictive than the 'user' tag. + It is logically opposite to the 'notAuthenticated' tag. + + + + + notAuthenticated + org.apache.shiro.web.tags.NotAuthenticatedTag + JSP + Displays body content only if the current user has NOT succesfully authenticated + _during their current session_. It is logically opposite to the 'authenticated' tag. + + + + + user + org.apache.shiro.web.tags.UserTag + JSP + Displays body content only if the current Subject has a known identity, either + from a previous login or from 'RememberMe' services. Note that this is semantically different + from the 'authenticated' tag, which is more restrictive. It is logically + opposite to the 'guest' tag. + + + + + guest + org.apache.shiro.web.tags.GuestTag + JSP + Displays body content only if the current Subject IS NOT known to the system, either + because they have not logged in or they have no corresponding 'RememberMe' identity. It is logically + opposite to the 'user' tag. + + + + + principal + org.apache.shiro.web.tags.PrincipalTag + JSP + Displays the user's principal or a property of the user's principal. + + type + false + true + + + property + false + true + + + defaultValue + false + true + + + + diff --git a/test_input/ksa/ksa-web-core/src/main/scripts/ksa-shiro.tld b/test_input/ksa/ksa-web-core/src/main/scripts/ksa-shiro.tld new file mode 100644 index 0000000..79913a4 --- /dev/null +++ b/test_input/ksa/ksa-web-core/src/main/scripts/ksa-shiro.tld @@ -0,0 +1,158 @@ + + + + + + 1.1.2 + 1.2 + Apache Shiro Extension + /shiro-tags + Extended Apache Shiro JSP Tag Library. + + + hasAnyPermissions + com.ksa.shiro.web.tags.HasAnyPermissions + JSP + Displays body content only if the current user has one of the specified permissions from a + comma-separated list of permissions. + + + name + true + true + + + + + hasPermission + org.apache.shiro.web.tags.HasPermissionTag + JSP + Displays body content only if the current Subject (user) + 'has' (implies) the specified permission (i.e the user has the specified ability). + + + name + true + true + + + + + lacksPermission + org.apache.shiro.web.tags.LacksPermissionTag + JSP + Displays body content only if the current Subject (user) does + NOT have (not imply) the specified permission (i.e. the user lacks the specified ability) + + + name + true + true + + + + + hasRole + org.apache.shiro.web.tags.HasRoleTag + JSP + Displays body content only if the current user has the specified role. + + name + true + true + + + + + + hasAnyRoles + org.apache.shiro.web.tags.HasAnyRolesTag + JSP + Displays body content only if the current user has one of the specified roles from a + comma-separated list of role names. + + + name + true + true + + + + + lacksRole + org.apache.shiro.web.tags.LacksRoleTag + JSP + Displays body content only if the current user does NOT have the specified role + (i.e. they explicitly lack the specified role) + + + name + true + true + + + + + authenticated + org.apache.shiro.web.tags.AuthenticatedTag + JSP + Displays body content only if the current user has successfully authenticated + _during their current session_. It is more restrictive than the 'user' tag. + It is logically opposite to the 'notAuthenticated' tag. + + + + + notAuthenticated + org.apache.shiro.web.tags.NotAuthenticatedTag + JSP + Displays body content only if the current user has NOT succesfully authenticated + _during their current session_. It is logically opposite to the 'authenticated' tag. + + + + + user + org.apache.shiro.web.tags.UserTag + JSP + Displays body content only if the current Subject has a known identity, either + from a previous login or from 'RememberMe' services. Note that this is semantically different + from the 'authenticated' tag, which is more restrictive. It is logically + opposite to the 'guest' tag. + + + + + guest + org.apache.shiro.web.tags.GuestTag + JSP + Displays body content only if the current Subject IS NOT known to the system, either + because they have not logged in or they have no corresponding 'RememberMe' identity. It is logically + opposite to the 'user' tag. + + + + + principal + org.apache.shiro.web.tags.PrincipalTag + JSP + Displays the user's principal or a property of the user's principal. + + type + false + true + + + property + false + true + + + defaultValue + false + true + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/pom.xml b/test_input/ksa/ksa-web-root/ksa-bd-web/pom.xml new file mode 100644 index 0000000..b104261 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + + com.ksa + ksa-web-root + 3.9.0 + + + ksa-bd-web + jar + + ksa-bd-web + 杭州凯思爱物流管理系统 - 基础数据管理 WEB 模块 + + + UTF-8 + + + + + com.ksa + ksa-security-web + ${project.version} + + + com.ksa + ksa-bd-service + ${project.version} + + + diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/DateRateAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/DateRateAction.java new file mode 100644 index 0000000..d827c26 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/DateRateAction.java @@ -0,0 +1,67 @@ +package com.ksa.web.struts2.action.bd.currency; + +import java.util.Calendar; + +public class DateRateAction extends RateAction { + + private static final long serialVersionUID = -5973328793188457985L; + + protected int yyyy; + protected int mm; + + public int getYyyy() { + return this.yyyy; + } + + public void setYyyy( int yyyy ) { + this.yyyy = yyyy; + } + + // 月份用 month 表示 freemarker 不认 + public int getMm() { + return this.mm; + } + + public void setMm( int mm ) { + this.mm = mm; + } + + @Override + public void validate() { + super.validate(); + + Calendar d = Calendar.getInstance(); + int currentYear = d.get( Calendar.YEAR ); + int currentMonth = d.get( Calendar.MONTH ) + 1; + + // 未设置年份和月份,则默认为当前月份 + if( yyyy == 0 ) { + this.yyyy = currentYear; + } + if( mm == 0 ) { + this.mm = currentMonth; + } + + // 不允许查询未来的汇率,因为未来无法预知! + if( yyyy > currentYear ) { + this.yyyy = currentYear; + this.mm = currentMonth; + } else if( yyyy == currentYear && mm > currentMonth ) { + this.mm = currentMonth; + } + + // 验证 设置非法的年份和月份 + if( mm < 1 ) { + mm = 1; + } + if( mm > 12 ) { + mm = 12; + } + if( yyyy < 1980 ) { + yyyy = 1980; + } + if( yyyy > 2100 ) { + yyyy = 2100; + } + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/PartnerRateAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/PartnerRateAction.java new file mode 100644 index 0000000..9dfb9d8 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/PartnerRateAction.java @@ -0,0 +1,26 @@ +package com.ksa.web.struts2.action.bd.currency; + +import org.springframework.util.StringUtils; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.bd.Partner; +import com.ksa.service.bd.PartnerService; + +public class PartnerRateAction extends RateAction { + + private static final long serialVersionUID = -8059608682104160166L; + + @Override + protected String doExecute() throws Exception { + if( StringUtils.hasText( rate.getPartner().getId() ) ) { + PartnerService s = ServiceContextUtils.getService( PartnerService.class ); + try { + rate.setPartner( s.loadPartnerById( rate.getPartner().getId() ) ); + return SUCCESS; + } catch( RuntimeException e ) { } + } + // 设置partner为空 + rate.setPartner( new Partner() ); + return SUCCESS; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateAction.java new file mode 100644 index 0000000..7b4e03a --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateAction.java @@ -0,0 +1,31 @@ +package com.ksa.web.struts2.action.bd.currency; + +import com.ksa.model.bd.CurrencyRate; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + +public class RateAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = -6133119525401391420L; + + protected CurrencyRate rate = new CurrencyRate(); + + @Override + public String execute() throws Exception { + // 权限校验 + if( ! SecurityUtils.isPermitted( "bd:currency" ) ) { + return NO_PERMISSION; + } + return super.execute(); + } + + @Override + public CurrencyRate getModel() { + return getRate(); + } + + public CurrencyRate getRate() { + return rate; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateSaveAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateSaveAction.java new file mode 100644 index 0000000..2519532 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/RateSaveAction.java @@ -0,0 +1,31 @@ +package com.ksa.web.struts2.action.bd.currency; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + +public class RateSaveAction extends RateAction implements JsonAction { + + private static final long serialVersionUID = 605636145713159434L; + + private JsonResult result; + + @Override + protected String doExecute() throws Exception { + CurrencyRateService service = ServiceContextUtils.getService( CurrencyRateService.class ); + service.saveCurrencyRate( rate ); + String message = String.format( "成功设置货币汇率。"); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message, rate ); + return SUCCESS; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next(), rate ); + } + return this.result; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/DateRateGridDataAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/DateRateGridDataAction.java new file mode 100644 index 0000000..13afd90 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/DateRateGridDataAction.java @@ -0,0 +1,86 @@ +package com.ksa.web.struts2.action.bd.currency.data; + +import java.util.Calendar; +import java.util.List; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.web.struts2.action.data.GridDataActionSupport; + +public class DateRateGridDataAction extends GridDataActionSupport { + + private static final long serialVersionUID = -7565072588650494148L; + + protected int year; + protected int month; + + protected Object[] gridDataArray = new Object[0]; + + @Override + protected String doExecute() throws Exception { + CurrencyRateService service = ServiceContextUtils.getService( CurrencyRateService.class ); + Calendar c = Calendar.getInstance(); + c.set( year, month - 1, 1 ); + List rates = service.loadLatestCurrencyRates( c.getTime() ); + gridDataArray = rates.toArray(); + return SUCCESS; + } + + @Override + protected Object[] queryGridData() { + return gridDataArray; + } + + @Override + protected int queryGridDataCount() { + return gridDataArray.length; + } + + public void setYear( int year ) { + this.year = year; + } + + public void setMonth( int month ) { + this.month = month; + } + + @Override + public void validate() { + super.validate(); + + Calendar d = Calendar.getInstance(); + int currentYear = d.get( Calendar.YEAR ); + int currentMonth = d.get( Calendar.MONTH ) + 1; + + // 未设置年份和月份,则默认为当前月份 + if( year == 0 ) { + this.year = currentYear; + } + if( month == 0 ) { + this.month = currentMonth; + } + + // 不允许查询未来的汇率,因为未来无法预知! + if( year > currentYear ) { + this.year = currentYear; + this.month = currentMonth; + } else if( year == currentYear && month > currentMonth ) { + this.month = currentMonth; + } + + // 验证 设置非法的年份和月份 + if( month < 1 ) { + month = 1; + } + if( month > 12 ) { + month = 12; + } + if( year < 1980 ) { + year = 1980; + } + if( year > 2100 ) { + year = 2100; + } + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/PartnerRateGridDataAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/PartnerRateGridDataAction.java new file mode 100644 index 0000000..dcea5ba --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/currency/data/PartnerRateGridDataAction.java @@ -0,0 +1,55 @@ +package com.ksa.web.struts2.action.bd.currency.data; + +import java.util.List; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.bd.Partner; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.service.bd.PartnerService; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.data.GridDataActionSupport; + +public class PartnerRateGridDataAction extends GridDataActionSupport { + + private static final long serialVersionUID = 426472765049926642L; + + protected String partnerId; + + protected Object[] gridDataArray = new Object[0]; + + @Override + protected String doExecute() throws Exception { + if( StringUtils.hasText( partnerId ) ) { + PartnerService s = ServiceContextUtils.getService( PartnerService.class ); + try { + // FIXME : 这个验证似乎应该放入 service 层!! 确认传入的用户标识合法 + Partner partner = s.loadPartnerById( partnerId ); + + // 获取数据 + CurrencyRateService service = ServiceContextUtils.getService( CurrencyRateService.class ); + List rates = service.loadPartnerCurrencyRates( partner.getId() ); + gridDataArray = rates.toArray(); + return SUCCESS; + } catch( RuntimeException e ) { } + } + + return SUCCESS; + } + + @Override + protected Object[] queryGridData() { + return gridDataArray; + } + + @Override + protected int queryGridDataCount() { + return gridDataArray.length; + } + + public void setPartnerId( String partnerId ) { + this.partnerId = partnerId; + } + + +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataAction.java new file mode 100644 index 0000000..d0a9922 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataAction.java @@ -0,0 +1,59 @@ +package com.ksa.web.struts2.action.bd.data; + +import org.springframework.util.StringUtils; + +import com.ksa.model.bd.BasicData; +import com.ksa.model.bd.BasicDataType; +import com.ksa.service.bd.util.BasicDataUtils; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + +public class BasicDataAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = 6210546520116254817L; + + protected BasicData data; + + public BasicDataAction() { + data = new BasicData(); + if( BasicDataType.ALL_TYPE.length > 0 ) { + data.setType( new BasicDataType( BasicDataType.ALL_TYPE[0].getId(), BasicDataType.ALL_TYPE[0].getName() ) ); + } + } + + @Override + public String execute() throws Exception { + // 权限校验 + if( ! SecurityUtils.isPermitted( "bd:data" ) ) { + return NO_PERMISSION; + } + try { + String result = doExecute(); + updateBasicDataCache(); + return result; + } catch( RuntimeException e ) { + this.addActionError( e.getMessage() ); + return ERROR; + } + } + + protected void updateBasicDataCache() { + if( StringUtils.hasText( data.getId() ) ) { + BasicDataUtils.updateData( data.getId() ); + } + } + + @Override + public BasicData getModel() { + return getData(); + } + + public BasicData getData() { + return data; + } + + public BasicDataType[] getAllTypes() { + return BasicDataType.ALL_TYPE; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataDeleteAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataDeleteAction.java new file mode 100644 index 0000000..d7afd48 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataDeleteAction.java @@ -0,0 +1,42 @@ +package com.ksa.web.struts2.action.bd.data; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.BasicDataService; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + +public class BasicDataDeleteAction extends BasicDataAction implements JsonAction { + + private static final long serialVersionUID = 1698109281585224800L; + + private JsonResult result; + + @Override + public String doExecute() throws Exception { + BasicDataService service = ServiceContextUtils.getService( BasicDataService.class ); + data = service.removeBasicData( data ); + String message = String.format( "成功删除基本代码:'%s'。", data.getName() ); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message, data ); + return SUCCESS; + } + + @Override + public void validate() { + super.validate(); + // 基本代码标识 + if( !StringUtils.hasText( data.getId() ) ) { + this.addActionError( "请输入基本代码的标识信息。" ); + } + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next(), data ); + } + return this.result; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataEditAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataEditAction.java new file mode 100644 index 0000000..f3f11c5 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataEditAction.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.bd.data; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.BasicDataService; + +public class BasicDataEditAction extends BasicDataAction { + + private static final long serialVersionUID = 5219022222818831078L; + + @Override + public String doExecute() throws Exception { + BasicDataService service = ServiceContextUtils.getService( BasicDataService.class ); + data = service.loadBasicDataById( data.getId() ); + return SUCCESS; + } + + @Override + protected void updateBasicDataCache() { + // 不需要更新缓存 + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataInsertAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataInsertAction.java new file mode 100644 index 0000000..a11848a --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataInsertAction.java @@ -0,0 +1,52 @@ +package com.ksa.web.struts2.action.bd.data; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.BasicDataService; +import com.ksa.util.StringUtils; + +public class BasicDataInsertAction extends BasicDataAction { + + private static final long serialVersionUID = 4601199504765579351L; + + @Override + public String doExecute() throws Exception { + BasicDataService service = ServiceContextUtils.getService( BasicDataService.class ); + data = service.createBasicData( data ); + addActionMessage( String.format( "成功创建基本代码:'%s'。", data.getName() ) ); + return SUCCESS; + } + + @Override + public void validate() { + super.validate(); + final int basicDataLength = 200; + // 基本代码编码 + if( !StringUtils.hasText( data.getCode() ) ) { + this.addActionError( "请输入基本代码。" ); + } else { + if( data.getName().length() > basicDataLength ) { + this.addActionError( "基本代码过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + // 名称 + if( !StringUtils.hasText( data.getName() ) ) { + this.addActionError( "请输入基本代码名称。" ); + } else { + if( data.getName().length() > basicDataLength ) { + this.addActionError( "基本代码名称过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + // 别名 + if( StringUtils.hasText( data.getAlias() ) && data.getAlias().length() > basicDataLength ) { + this.addActionError( "基本代码别名过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + // 备注 + if( StringUtils.hasText( data.getNote() ) && data.getNote().length() > 2000 ) { + this.addActionError( "基本代码备注过长,请控制在 2000 个字符之内。" ); + } + // 附加值 + if( StringUtils.hasText( data.getExtra() ) && data.getExtra().length() > basicDataLength ) { + this.addActionError( "基本代码附加信息过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataUpdateAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataUpdateAction.java new file mode 100644 index 0000000..5a94033 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/data/BasicDataUpdateAction.java @@ -0,0 +1,56 @@ +package com.ksa.web.struts2.action.bd.data; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.BasicDataService; +import com.ksa.util.StringUtils; + +public class BasicDataUpdateAction extends BasicDataAction { + + private static final long serialVersionUID = -7483703710789322373L; + + @Override + public String doExecute() throws Exception { + BasicDataService service = ServiceContextUtils.getService( BasicDataService.class ); + data = service.modifyBasicData( data ); + addActionMessage( String.format( "成功更新基本代码:'%s'。", data.getName() ) ); + return SUCCESS; + } + + @Override + public void validate() { + super.validate(); + final int basicDataLength = 200; + // 基本代码标识 + if( !StringUtils.hasText( data.getId() ) ) { + this.addActionError( "请输入基本代码的标识。" ); + } + // 基本代码编码 + if( !StringUtils.hasText( data.getCode() ) ) { + this.addActionError( "请输入基本代码。" ); + } else { + if( data.getName().length() > basicDataLength ) { + this.addActionError( "基本代码过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + // 名称 + if( !StringUtils.hasText( data.getName() ) ) { + this.addActionError( "请输入基本代码名称。" ); + } else { + if( data.getName().length() > basicDataLength ) { + this.addActionError( "基本代码名称过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + // 别名 + if( StringUtils.hasText( data.getAlias() ) && data.getAlias().length() > basicDataLength ) { + this.addActionError( "基本代码别名过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + // 备注 + if( StringUtils.hasText( data.getNote() ) && data.getNote().length() > 2000 ) { + this.addActionError( "基本代码备注过长,请控制在 2000 个字符之内。" ); + } + // 附加值 + if( StringUtils.hasText( data.getExtra() ) && data.getExtra().length() > basicDataLength ) { + this.addActionError( "基本代码附加信息过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerAction.java new file mode 100644 index 0000000..9d534a8 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerAction.java @@ -0,0 +1,31 @@ +package com.ksa.web.struts2.action.bd.partner; + +import com.ksa.model.bd.Partner; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + +public class PartnerAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = -5228129634318355749L; + + protected Partner partner = new Partner(); + + @Override + public String execute() throws Exception { + // 权限校验 + if( ! SecurityUtils.isPermitted( "bd:partner" ) ) { + return NO_PERMISSION; + } + return super.execute(); + } + + @Override + public Partner getModel() { + return getPartner(); + } + + public Partner getPartner() { + return partner; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerDeleteAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerDeleteAction.java new file mode 100644 index 0000000..8bdc70d --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerDeleteAction.java @@ -0,0 +1,42 @@ +package com.ksa.web.struts2.action.bd.partner; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.PartnerService; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + +public class PartnerDeleteAction extends PartnerAction implements JsonAction { + + private static final long serialVersionUID = 8812278179336055682L; + + private JsonResult result; + + @Override + public String doExecute() throws Exception { + PartnerService service = ServiceContextUtils.getService( PartnerService.class ); + partner = service.removePartner( partner ); + String message = String.format( "成功冻结合作伙伴:'%s'。", partner.getName() ); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message, partner ); + return SUCCESS; + } + + @Override + public void validate() { + super.validate(); + // 合作伙伴标识 + if( !StringUtils.hasText( partner.getId() ) ) { + this.addActionError( "请输入合作伙伴信息的标识信息。" ); + } + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next(), partner ); + } + return this.result; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerEditAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerEditAction.java new file mode 100644 index 0000000..cbb0fe9 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerEditAction.java @@ -0,0 +1,16 @@ +package com.ksa.web.struts2.action.bd.partner; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.PartnerService; + +public class PartnerEditAction extends PartnerAction { + + private static final long serialVersionUID = -8451721824534508563L; + + @Override + public String doExecute() throws Exception { + PartnerService service = ServiceContextUtils.getService( PartnerService.class ); + partner = service.loadPartnerById( partner.getId() ); + return SUCCESS; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerExtraAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerExtraAction.java new file mode 100644 index 0000000..dd75150 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerExtraAction.java @@ -0,0 +1,72 @@ +package com.ksa.web.struts2.action.bd.partner; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.dao.bd.PartnerDao; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + + +public class PartnerExtraAction extends PartnerAction implements JsonAction { + + private static final long serialVersionUID = -9215324416967989426L; + + protected static final String ACTION_INSERT = "insert"; + protected static final String ACTION_DELETE = "delete"; + + private JsonResult result; + protected String extra; + protected String action; + + @Override + protected String doExecute() throws Exception { + if( ACTION_INSERT.equalsIgnoreCase( action ) ) { + return insert(); + } else if( ACTION_DELETE.equalsIgnoreCase( action ) ) { + return delete(); + } else { + return super.doExecute(); + } + } + + protected String insert() throws Exception { + PartnerDao dao = ServiceContextUtils.getService( PartnerDao.class ); + dao.insertPartnerExtra( partner, extra ); + String message = "成功添加抬头信息。"; + addActionMessage( message ); + result = new JsonResult( SUCCESS, message ); + return SUCCESS; + } + + protected String delete() throws Exception { + PartnerDao dao = ServiceContextUtils.getService( PartnerDao.class ); + dao.deletePartnerExtra( partner, extra ); + String message = "成功删除抬头信息。"; + addActionMessage( message ); + result = new JsonResult( SUCCESS, message ); + return SUCCESS; + } + + public String getExtra() { + return extra; + } + + public void setExtra( String extra ) { + this.extra = extra; + } + + public String getAction() { + return action; + } + + public void setAction( String action ) { + this.action = action; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next(), partner ); + } + return this.result; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerInsertAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerInsertAction.java new file mode 100644 index 0000000..d9c330c --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerInsertAction.java @@ -0,0 +1,61 @@ +package com.ksa.web.struts2.action.bd.partner; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.PartnerService; +import com.ksa.util.StringUtils; + +public class PartnerInsertAction extends PartnerAction { + + private static final long serialVersionUID = -1776587314175844741L; + + @Override + public String doExecute() throws Exception { + PartnerService service = ServiceContextUtils.getService( PartnerService.class ); + partner = service.createPartner( partner ); + addActionMessage( String.format( "成功创建合作伙伴信息:'%s'。", partner.getName() ) ); + return SUCCESS; + } + + @Override + public void validate() { + super.validate(); + final int basicDataLength = 200; + final int longDataLength = 2000; + // 合作伙伴编码 + if( !StringUtils.hasText( partner.getCode() ) ) { + this.addActionError( "请输入代码信息。" ); + } else { + if( partner.getCode().length() > basicDataLength ) { + this.addActionError( "代码信息过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + // 名称 + if( !StringUtils.hasText( partner.getName() ) ) { + this.addActionError( "请输入名称息。" ); + } else { + if( partner.getName().length() > basicDataLength ) { + this.addActionError( "名称信息过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + + // 别名 + if( StringUtils.hasText( partner.getAlias() ) && partner.getAlias().length() > longDataLength ) { + this.addActionError( "抬头信息过长,请控制在 " + longDataLength + " 个字符之内。" ); + } + + // 备注 + if( StringUtils.hasText( partner.getNote() ) && partner.getNote().length() > longDataLength ) { + this.addActionError( "备注信息过长,请控制在 " + longDataLength + " 个字符之内。" ); + } + + // 地址 + if( StringUtils.hasText( partner.getAddress() ) && partner.getAddress().length() > longDataLength ) { + this.addActionError( "地址信息过长,请控制在 " + longDataLength + " 个字符之内。" ); + } + + // 付款周期 + if( partner.getPp() <= 0 ) { + this.addActionError( "付款周期必须大于 0 天。" ); + } + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerUpdateAction.java b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerUpdateAction.java new file mode 100644 index 0000000..1a92e82 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/java/com/ksa/web/struts2/action/bd/partner/PartnerUpdateAction.java @@ -0,0 +1,65 @@ +package com.ksa.web.struts2.action.bd.partner; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.bd.PartnerService; +import com.ksa.util.StringUtils; + +public class PartnerUpdateAction extends PartnerAction { + + private static final long serialVersionUID = 1198212413777749183L; + + @Override + public String doExecute() throws Exception { + PartnerService service = ServiceContextUtils.getService( PartnerService.class ); + partner = service.modifyPartner( partner ); + addActionMessage( String.format( "成功更新合作伙伴信息:'%s'。", partner.getName() ) ); + return SUCCESS; + } + + @Override + public void validate() { + super.validate(); + final int basicDataLength = 200; + final int longDataLength = 2000; + // 合作伙伴标识 + if( !StringUtils.hasText( partner.getId() ) ) { + this.addActionError( "请输入标识信息。" ); + } + // 合作伙伴编码 + if( !StringUtils.hasText( partner.getCode() ) ) { + this.addActionError( "请输入代码信息。" ); + } else { + if( partner.getCode().length() > basicDataLength ) { + this.addActionError( "代码信息过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + // 名称 + if( !StringUtils.hasText( partner.getName() ) ) { + this.addActionError( "请输入名称息。" ); + } else { + if( partner.getName().length() > basicDataLength ) { + this.addActionError( "名称信息过长,请控制在 " + basicDataLength + " 个字符之内。" ); + } + } + + // 别名 + if( StringUtils.hasText( partner.getAlias() ) && partner.getAlias().length() > longDataLength ) { + this.addActionError( "抬头信息过长,请控制在 " + longDataLength + " 个字符之内。" ); + } + + // 备注 + if( StringUtils.hasText( partner.getNote() ) && partner.getNote().length() > longDataLength ) { + this.addActionError( "备注信息过长,请控制在 " + longDataLength + " 个字符之内。" ); + } + + // 地址 + if( StringUtils.hasText( partner.getAddress() ) && partner.getAddress().length() > longDataLength ) { + this.addActionError( "地址信息过长,请控制在 " + longDataLength + " 个字符之内。" ); + } + + // 付款周期 + if( partner.getPp() <= 0 ) { + this.addActionError( "付款周期必须大于 0 天。" ); + } + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/js/bd/utils.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/js/bd/utils.js new file mode 100644 index 0000000..1b7e02f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/js/bd/utils.js @@ -0,0 +1,38 @@ +(function($){ + /* KSA - bd utils 初始化命名空间 */ + ksa.bd = ksa.bd || {}; + $.extend( ksa.bd, { + /** + * 弹出选择单位类型窗口 + */ + selectDepartments : function( callback ) { + top.$.open( { + src : ksa.buildUrl( "/component/bd", "department-selection" ), + title : "选择单位类型" + }, callback ); + }, + /** + * 弹出选择单位抬头窗口 + */ + selectPartnerAlias : function( callback, partnerId ) { + top.$.open( { + width: 600, + height: 400, + src : ksa.buildUrl( "/component/bd", "partner-alias-selection", { "partner.id" : partnerId } ), + title : "选择单位抬头" + }, callback ); + }, + /** + * 选择并替换单位抬头 + */ + replacePartnerAlias : function( partnerId, selector ) { + ksa.bd.selectPartnerAlias( function( alias ) { + try{ + if( (typeof alias) == "string" && alias ) { + $( selector ).val( alias ); + } + }catch(e){} + }, partnerId ); + }, + } ); +})(jQuery); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts-plugin.xml b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts-plugin.xml new file mode 100644 index 0000000..85ccf87 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts-plugin.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-component.xml b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-component.xml new file mode 100644 index 0000000..d7ddc4f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-component.xml @@ -0,0 +1,27 @@ + + + + + + + + /ui/bd/component/department-selection.ftl + + + + + /ui/bd/component/partner-alias-selection.ftl + + + + insert + + + + delete + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-currency.xml b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-currency.xml new file mode 100644 index 0000000..254ac52 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-currency.xml @@ -0,0 +1,40 @@ + + + + + + /ui/bd/currency/default.ftl + + + + + + + + + + + + /ui/bd/currency/date/default.ftl + + + + + + + /ui/bd/currency/partner/default.ftl + + + + + + + + + + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-data.xml b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-data.xml new file mode 100644 index 0000000..dc100fb --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-data.xml @@ -0,0 +1,40 @@ + + + + + + /ui/bd/data/default.ftl + + + + + + + /ui/bd/data/create-data.ftl + + + /ui/bd/data/edit-data.ftl + /ui/bd/data/create-data.ftl + /ui/bd/data/create-data.ftl + + + + /ui/bd/data/edit-data.ftl + /ui/bd/data/edit-data.ftl + /ui/bd/data/edit-data.ftl + + + /ui/bd/data/edit-data.ftl + /ui/bd/data/edit-data.ftl + /ui/bd/data/edit-data.ftl + + + + + + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-partner.xml b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-partner.xml new file mode 100644 index 0000000..ba9c800 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/struts2/struts-bd-partner.xml @@ -0,0 +1,40 @@ + + + + + + /ui/bd/partner/default.ftl + + + + + + + /ui/bd/partner/create-partner.ftl + + + /ui/bd/partner/edit-partner.ftl + /ui/bd/partner/create-partner.ftl + /ui/bd/partner/create-partner.ftl + + + + /ui/bd/partner/edit-partner.ftl + /ui/bd/partner/edit-partner.ftl + /ui/bd/partner/edit-partner.ftl + + + /ui/bd/partner/edit-partner.ftl + /ui/bd/partner/edit-partner.ftl + /ui/bd/partner/edit-partner.ftl + + + + + + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.ftl new file mode 100644 index 0000000..2a781a6 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.ftl @@ -0,0 +1,25 @@ +<#-- + 多单位类型选择界面 + 装饰页面:WEB-INF/decorators/component.jsp + --> + + +单位类型选择 + + + + +
+
+
+ + +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.js new file mode 100644 index 0000000..0cdd493 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/department-selection.js @@ -0,0 +1,42 @@ +$(function(){ + // 确认选择 + $("#btn_ok").click( function() { + var results = $("#multiple_selection").multipleselection("getResults"); + parent.$.close( results ); + return false; + }); + + var $grid = $("
"); + // 初始化多选组件 + $("#multiple_selection").multipleselection({ + dataSectionTitle : "备选单位类型列表", + getSelectedData:function() { + var data = []; + var rows = $grid.datagrid('getSelections'); + for(var i=0;i + + +单位抬头选择 + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ + +
+
+
+ + +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/partner-alias-selection.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/partner-alias-selection.js new file mode 100644 index 0000000..1cb68d9 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/component/partner-alias-selection.js @@ -0,0 +1,120 @@ +$(function() { + + /* ------------ 合作伙伴信息 ------------ */ + $( "#partner_id" ).combobox( { + url : ksa.buildUrl( "/data/combo", "bd-partner-all" ), + onSelect : function( record ) { + $grid.datagrid( "load", { id : record.id } ); + } + } ); + + + // 确认选择 + $("#btn_ok").click( function() { + var results = $("#extra_grid").datagrid("getSelected"); + parent.$.close( results ); + return false; + }); + + // 添加确认 + $("#btn_extra_ok").click( function() { + $("#btn_extra_ok").attr("disabled", "disabled"); + var extra = $("#extra").val(); + if( ! extra ) { + top.$.messager.warning("请输入新建的抬头信息。"); + $("#btn_extra_ok").attr("disabled", null ); + return false; + } else { + // 保存 + $.ajax({ + url: ksa.buildUrl( "/component/bd", "partner-alias-insert" ), + data: { "partner.id" : $("#partner_id").combobox("getValue"), extra : extra }, + success: function( result ) { + try { + if (result.status == "success") { // 添加成功 + parent.$.close( extra ); + return false; + } + else { $.messager.error( result.message ); $("#btn_extra_ok").attr("disabled", null ); } + } catch (e) { $("#btn_extra_ok").attr("disabled", null ); } + } + }); + } + } ); + // 添加关闭 + $("#btn_extra_close").click( function() { + $("#extra_window").window("close"); + } ); + + // 单位别名 + var NEW_LINE = "\n"; + $.fn.datagrid.defaults.loadEmptyMsg = '注意 没有获取到任何数据,请选择新的合作单位。'; + var $grid = $('#extra_grid').datagrid({ + title : '抬头信息:' + PARTNER_NAME, + url: ksa.buildUrl( "/data/grid", "bd-partner-extra" ), + pagination : false, + queryParams : { + id : $("#partner_id").combobox("getValue") + }, + fit : true, + onDblClickRow : function( i, data ) { + parent.$.close( data ); + return false; + }, + columns:[[ + { field:'dump', checkbox:true }, + { field:'name', title:'抬头', width:200, formatter:function(v,data,i) { + var a = data; + try { while( a.indexOf( NEW_LINE ) >= 0 ) { a = a.replace( NEW_LINE, "
" ); } return a; } + catch(e) { return data; } + } } + ]], + toolbar:[{ + text:'添加...', + cls: 'btn-primary', + iconCls:'icon-plus icon-white', + handler:function(){ + var id = $("#partner_id").combobox("getValue"); + if( !id || id == "" ) { + top.$.messager.warning("请首先选择合作单位,再进行抬头信息的添加操作。"); + return; + } + $("#extra_window").window("open"); + $("#extra").val(""); + try { $("#extra")[0].focus(); } catch(e){} + } + }, '-', { + text:'删除', + cls: 'btn-danger', + iconCls:'icon-trash icon-white', + handler:function(){ deleteExtra(); } + }] + }); + + // 删除 + function deleteExtra() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + top.$.messager.warning("请选择一条数据后,再进行删除操作。"); + return; + } + + $.messager.confirm( "确定删除所选抬头吗?", function( ok ){ + if( ok ) { + $.ajax({ + url: ksa.buildUrl( "/component/bd", "partner-alias-delete" ), + data: { "partner.id" : $("#partner_id").combobox("getValue"), extra : $grid.datagrid("getSelected") }, + success: function( result ) { + try { + if (result.status == "success") { + $.messager.success( result.message ); + $grid.datagrid( "reload" ); + } + else { $.messager.error( result.message ); } + } catch (e) { } + } + }); + } + } ); + } +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.ftl new file mode 100644 index 0000000..6d0d75a --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.ftl @@ -0,0 +1,26 @@ + + +每月货币汇率 + + + + +
+
+

每月汇率列表 ${yyyy?c}年${mm?c}月

+ 年 + 月 + + +
+
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.js new file mode 100644 index 0000000..82980b7 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/date/default.js @@ -0,0 +1,76 @@ +$(function(){ + // 上次编辑的行编号 + var lastIndex = -1; + + var year = $("#yy").val(); + var month = $("#mm").val(); + var now = new Date(); + + // 初始化汇率表 + var $grid = $('#data-grid').datagrid({ + url : ksa.buildUrl( "/data/grid/currency", "date", { "year" : year, "month" : month } ), + height : $(window).height() - 65, + pagination : false, + fitColumns : false, + columns : [ [ + { field:'code', title:'货币代码', width:100, + formatter: function(v, data ){ try{return data.currency.code;}catch(e){return "";} } }, + { field:'name', title:'货币名称', width:100, + formatter: function(v, data ){ try{return data.currency.name;}catch(e){return "";} } }, + { field:'rate', title:'汇率', width:100, align:'right', editor:{ type:'numberbox',options:{precision:4} }, + styler:function(){return 'color:blue;';} }, + ] ], + rowStyler:function(index,row,css){ + if ( !row.id ){ + return 'color:red;font-weight:bold;'; + } + }, + onClickRow:function( rowIndex ) { + if (lastIndex != rowIndex){ + if( lastIndex >= 0 ) { + save(); + } + $grid.datagrid('beginEdit', rowIndex); + } + lastIndex = rowIndex; + } + + }); + + // 绑定保存事件 + $("#save").unbind("click").bind("click", function(e){ + var length = save(); + if( length > 0 ) { + $.messager.success("货币汇率设置成功!"); + } else { + $.messager.info("暂时没有任何修改需要保存。"); + } + return false; + }); + + function save() { + if( lastIndex >= 0 ) { $grid.datagrid('endEdit', lastIndex); } + var rates = $grid.datagrid('getChanges'); + $grid.datagrid('acceptChanges'); + + $.each( rates, function(i, rate) { + var rowIndex = $grid.datagrid( 'getRowIndex', rate ); + $.ajax({ + url: ksa.buildUrl( "/ui/bd/currency", "save" ), + data: { "id": rate.id, "rate": rate.rate, "currency.id": rate.currency.id, "month": (year + "-" + month + "-1") }, + success: function( result ) { + try { + if (result.status == "success") { + $grid.datagrid( "updateRow",{ index:rowIndex,row:result.data } ); + } + else { $.messager.error( result.message ); } + } catch (e) { } + } + } ); + } ); + return rates.length; // 返回更新数量 + } + + // 绑定热键事件 + ksa.hotkey.bindButton( $(".toolbar button.btn") ); +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.ftl new file mode 100644 index 0000000..82147ab --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.ftl @@ -0,0 +1,15 @@ + + + +货币汇率管理 + + + +
+
'">
+
'">
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.js new file mode 100644 index 0000000..f71d7b7 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/default.js @@ -0,0 +1,8 @@ +$(function(){ + // 初始化 tabs + var $tabs = $("#container").tabs({ + height: $(window).height() - 50 + }); + // 激活菜单 + ksa.menu.activate( "#_sidebar_bd_currency" ); +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.ftl new file mode 100644 index 0000000..3bd1828 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.ftl @@ -0,0 +1,24 @@ + + +每月货币汇率 + + + + +
+
+

客户汇率列表 <#if model.partner.name??>${model.partner.name!}

+ 选择客户: + + + +
+
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.js new file mode 100644 index 0000000..5142c5f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/currency/partner/default.js @@ -0,0 +1,75 @@ +$(function(){ + // 上次编辑的行编号 + var lastIndex = -1; + var partnerId = $("#partner").val(); + // 初始化客户选择框 + $('#partner').combobox({ url: ksa.buildUrl( "/data/combo", "bd-partner-all" ) }); + + // 初始化汇率表 + var $grid = $('#data-grid').datagrid({ + url : ksa.buildUrl( "/data/grid/currency", "partner", { "partnerId" : partnerId } ), + height : $(window).height() - 65, + pagination : false, + fitColumns : false, + columns : [ [ + { field:'code', title:'货币代码', width:100, + formatter: function(v, data ){ try{return data.currency.code;}catch(e){return "";} } }, + { field:'name', title:'货币名称', width:100, + formatter: function(v, data ){ try{return data.currency.name;}catch(e){return "";} } }, + { field:'rate', title:'汇率', width:100, align:'right', editor:{ type:'numberbox',options:{precision:4} }, + styler:function(){return 'color:blue;';} }, + ] ], + rowStyler:function(index,row,css){ + if ( !row.id ){ + return 'color:red;font-weight:bold;'; + } + }, + onClickRow:function( rowIndex ) { + if (lastIndex != rowIndex){ + if( lastIndex >= 0 ) { + save(); + } + $grid.datagrid('beginEdit', rowIndex); + } + lastIndex = rowIndex; + } + + }); + + // 绑定保存事件 + $("#save").unbind("click").bind("click", function(e){ + var length = save(); + if( length > 0 ) { + $.messager.success("货币汇率设置成功!"); + } else { + $.messager.info("暂时没有任何修改需要保存。"); + } + return false; + }); + + function save() { + if( lastIndex >= 0 ) { $grid.datagrid('endEdit', lastIndex); } + var rates = $grid.datagrid('getChanges'); + $grid.datagrid('acceptChanges'); + + $.each( rates, function(i, rate) { + var rowIndex = $grid.datagrid( 'getRowIndex', rate ); + $.ajax({ + url: ksa.buildUrl( "/ui/bd/currency", "save" ), + data: { "id": rate.id, "rate": rate.rate, "currency.id": rate.currency.id, "partner.id": partnerId }, + success: function( result ) { + try { + if (result.status == "success") { + $grid.datagrid( "updateRow",{ index:rowIndex,row:result.data } ); + } + else { $.messager.error( result.message ); } + } catch (e) { } + } + } ); + } ); + return rates.length; // 返回更新数量 + } + + // 绑定热键事件 + ksa.hotkey.bindButton( $(".toolbar button.btn") ); +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/create-data.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/create-data.ftl new file mode 100644 index 0000000..07eab5b --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/create-data.ftl @@ -0,0 +1,68 @@ + + +新建基本代码 + + + +
"> +
+ <@s.actionerror /> + <@s.actionmessage /> + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ <#if data.type.id == "00-currency"> +
+ +
+ +
+
+ <#else> + + +
+ +
+ +
+
+
+ +
+ +
+
+ <#-- end : control-group --> + +
<#-- end : tab-panel 基本属性 --> +
+ + +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.ftl new file mode 100644 index 0000000..fdd439f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.ftl @@ -0,0 +1,39 @@ + + + +基本代码管理 + + + + +
+

基本代码

+ + + + +
+ +
+ +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.js new file mode 100644 index 0000000..15ca63e --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/default.js @@ -0,0 +1,129 @@ +$(function(){ + var $CURRENCY_ID = '00-currency'; + var $grid = null; + var $typeId = TYPE_ID; + var $column = [ + { field:'code', title:'代码', width:50, sortable:true }, + { field:'name', title:'中文名称', width:100, sortable:true }, + { field:'alias', title:'英文名称', width:100, sortable:true}, + { field:'extra', title:'汇率', width:100, sortable:true, + hidden:true, align:'right',styler:function(){return 'color:blue;';}}, + { field:'note', title:'备注', width:200, sortable:true}, + { field:'rank', title:'排序', width:30, sortable:true, align:'right'} + ]; + + // 点击数据种类 过滤列表 + $("#type_list > li").click(function(){ + $typeId = $(this).attr("id"); + var typeName = $("span.type-name", $(this)).text(); + if( $typeId == $CURRENCY_ID ) { + $column[3].hidden = false; + } else { + $column[3].hidden = true; + } + initDataGrid( $typeId, typeName, $column ); + ksa.menu.deactivate( "#type_list > li" ); + $("#type_list li .label-warning").addClass("label-info").removeClass("label-warning"); + ksa.menu.activate( this ); + $(".label", $(this)).addClass("label-warning").removeClass("label-info"); + }); + + // 添加事件 + $("#btn-add").click( function() { + $.open({ + width:600, + height:440, + title:"新建基本代码 - " + $("#type_name").text( ), + iconCls : "icon-book", + src : ksa.buildUrl( "/dialog/bd/data", "create", { 'type.id' : $typeId } ) + }, function(){ + $grid.datagrid( "reload" ); + }); + }); + + // 编辑事件 + $("#btn-edit").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行编辑操作。"); + return; + } + + // 打开编辑页面 + $.open({ + width:600, + height:440, + title:"编辑基本代码:" + row.name, + iconCls : "icon-book", + src : ksa.buildUrl( "/dialog/bd/data", "edit", { id : row.id } ) + }, function(){ + $grid.datagrid( "reload" ); + }); + + }); + + // 删除事件 + $("#btn-delete").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行删除操作。"); + return; + } + + $.messager.confirm( "确定删除基本代码 '" + row.name + "' 吗?", function( ok ){ + if( ok ) { + $.ajax({ + url: ksa.buildUrl( "/dialog/bd/data", "delete" ), + data: { id : row.id }, + success: function( result ) { + try { + if (result.status == "success") { + $.messager.success( result.message ); + $grid.datagrid( "reload" ); + } + else { $.messager.error( result.message ); } + } catch (e) { } + } + }); + } + } ); + + }); + + // 初始化列表 + function initDataGrid( typeId, typeName, column ) { + $grid = $('#data-grid').datagrid({ + url: ksa.buildUrl( "/data/grid", "bd-data-bytype", { typeId : typeId } ), + pagination:true, + height : $(window).height() - 100, + columns:[ column ], + onDblClickRow : function() { + $("#btn-edit").click(); + } + }); + + $("#type_name").text( typeName ); + } + + // 初始化 激活第一个基本菜单项 + $("li#" + $typeId).click(); + // 绑定热键事件 + ksa.hotkey.bindButton( $(".toolbar button.btn") ); + //ksa.hotkey.bindDatagrid( $grid ); + // 激活菜单 + ksa.menu.activate( "#_sidebar_bd_data" ); + + $("#btn_search").click( function() { + var search = $.trim( $("#txt_search").val() ); + if( !search ) { + $("#btn_clear").click(); + } else { + $grid.datagrid("reload", { "search":search } ); + } + } ); + + $("#btn_clear").click( function() { + $grid.datagrid("reload", { "search":undefined }); + $("#txt_search").val(""); + } ); +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/edit-data.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/edit-data.ftl new file mode 100644 index 0000000..0b9ba61 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/data/edit-data.ftl @@ -0,0 +1,69 @@ + + +基本代码 :${type.name!} + + + +
"> +
+ <@s.actionerror /> + <@s.actionmessage /> + + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ <#if data.type.id == "00-currency"> +
+ +
+ +
+
+ <#else> + + +
+ +
+ +
+
+
+ +
+ +
+
+ <#-- end : control-group --> + +
<#-- end : tab-panel 基本属性 --> +
+ + +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/create-partner.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/create-partner.ftl new file mode 100644 index 0000000..0f40846 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/create-partner.ftl @@ -0,0 +1,114 @@ + + +新建合作伙伴信息 + + + + +
"> +
+
+ <@s.actionerror /> + <@s.actionmessage /> +
+
+ +
+ +
+
+
+ +
+ +
+

+
+ +
+ +
+
+
+ +
+ + 天付款 +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
<#-- end : tab-panel 基本属性 --> +
+
+ + +
+
+
+
+
+ + +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.ftl new file mode 100644 index 0000000..8735192 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.ftl @@ -0,0 +1,34 @@ + + + +合作伙伴信息管理 + + + + +
+

合作伙伴信息列表

+ + + + +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.js new file mode 100644 index 0000000..7e3e165 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/default.js @@ -0,0 +1,111 @@ +$(function(){ + var $winWidth = 780; + var $winHeight = 450; + var $grid = null; + $grid = $('#data-grid').datagrid({ + url: ksa.buildUrl( "/data/grid", "bd-partner-all" ), + height: $(window).height() - 105, + pageSize: 20, + columns:[ [ + { field:'important', title:'状态', width:25, sortable:true, align:'center', + formatter: function(value) { + if( value > 0 ) { return ""; } + else if( value < 0 ) { return ""; } + else { return ""; } + } + }, + { field:'code', title:'代码', width:100, sortable:true }, + { field:'name', title:'名称', width:100, sortable:true }, + { field:'alias', title:'抬头', width:200, sortable:true }, + { field:'address', title:'地址', width:200, sortable:true}, + { field:'saler_name', title:'销售担当', width:50, sortable:true, align:'center', + formatter: function(value, data ){ try{return data.saler.name;}catch(e){return "";} } }, + { field:'pp', title:'付款周期', width:50, sortable:true, align:'center', formatter: function(value){return "" + value + " 天付款";} }, + { field:'rank', title:'排序', width:25, sortable:true, align:'right'} + ] ], + onDblClickRow : function() { + $("#btn-edit").click(); + } + }); + + // 添加事件 + $("#btn-add").click( function() { + $.open({ + width:$winWidth, + height:$winHeight, + + title:"新建合作伙伴信息", + src : ksa.buildUrl( "/dialog/bd/partner", "create" ) + }, function(){ + $grid.datagrid( "reload" ); + }); + }); + + // 编辑事件 + $("#btn-edit").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行编辑操作。"); + return; + } + + // 打开编辑页面 + $.open({ + width:$winWidth, + height:$winHeight, + + title:"编辑合作伙伴信息:" + row.name, + src : ksa.buildUrl( "/dialog/bd/partner", "edit", { id : row.id } ) + }, function(){ + $grid.datagrid( "reload" ); + }); + + }); + + // 删除事件 + $("#btn-delete").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行删除操作。"); + return; + } + + $.messager.confirm( "确定冻结合作伙伴 '" + row.name + "' 吗?", function( ok ){ + if( ok ) { + $.ajax({ + url: ksa.buildUrl( "/dialog/bd/partner", "delete" ), + data: { id : row.id }, + success: function( result ) { + try { + if (result.status == "success") { + $.messager.success( result.message ); + $grid.datagrid( "reload" ); + } + else { $.messager.error( result.message ); } + } catch (e) { } + } + }); + } + } ); + + }); + + // 绑定热键事件 + ksa.hotkey.bindButton( $(".toolbar button.btn") ); + // 激活菜单 + ksa.menu.activate( "#_sidebar_bd_partner" ); + + $("#btn_search").click( function() { + var search = $.trim( $("#txt_search").val() ); + if( !search ) { + $("#btn_clear").click(); + } else { + $grid.datagrid("reload", { "search":search } ); + } + } ); + + $("#btn_clear").click( function() { + $grid.datagrid("reload", { "search":undefined }); + $("#txt_search").val(""); + } ); +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/edit-partner.ftl b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/edit-partner.ftl new file mode 100644 index 0000000..5f54461 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/edit-partner.ftl @@ -0,0 +1,124 @@ + + +合作伙伴信息:${partner.name!""} + + + + +
"> +
+
+ <@s.actionerror /> + <@s.actionmessage /> + +
+
+ +
+ +
+
+
+ +
+ +
+

+
+ +
+ +
+
+
+ +
+ + 天付款 +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
style="display:none"> + +
+ checked="checked"> +
+
+
+
+
+
<#-- end : tab-panel 基本属性 --> +
+
+ + +
+
+
+
+
+ + 冻结: + checked="checked"> + + + +
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/partner.js b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/partner.js new file mode 100644 index 0000000..0dd460b --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/ui/bd/partner/partner.js @@ -0,0 +1,104 @@ +$(function(){ + + // 单位类型 + var $grid = $('#data_grid').datagrid({ + title : '单位类型', + url: ksa.buildUrl( "/data/grid", "bd-partnertype-bypartnerid", { partnerId : $("#partner_id").val() } ), + pagination : false, + singleSelect: false, + height: 250, + columns:[[ + { field:'dump', checkbox:true }, + { field:'name', title:'单位类型', width:100, formatter:function(v,data,i) { + return v + ""; + } } + ]], + toolbar:[{ + text:'添加...', + cls : 'btn-primary', + iconCls:'icon-plus icon-white', + handler:function(){ + ksa.bd.selectDepartments( function( data ) { + var rowsCount = $grid.datagrid("getData").total; + var rows = $grid.datagrid("getData").rows; + for( var i = 0; i < data.length; i++ ) { + var duplicate = false; + for( var j = 0; j < rowsCount; j++ ) { + if( data[i].value == rows[j].id ) { + duplicate = true; break; + } + } + if( !duplicate ) { + var row = { id: data[i].value, name: data[i].text }; + $grid.datagrid( "appendRow", row ); + } + } + } ); + } + },'-',{ + text:'移除', + cls : 'btn-danger', + iconCls:'icon-trash icon-white', + handler:function(){ + var selectedRows = $grid.datagrid("getSelections"); + $.each( selectedRows, function(i,row) { + var index = $grid.datagrid('getRowIndex', row); + $grid.datagrid('deleteRow', index); + } ); + } + }] + }); + + // 初始化销售担当 + $("#saler").combobox({ + url : ksa.buildUrl( "/data/combo", "security-user-all" ), + codeField : "id", + width : 112 + }); + + function addTextarea( text ) { + var textarea = $("").text( text ).click(function(e){ + e.stopPropagation(); + }); + $("
").append( textarea ).appendTo( $("#text_container") ).mouseover( function(){ + $(this).addClass("textrow-hover"); + } ).mouseout( function(){ + $(this).removeClass("textrow-hover"); + } ).click( function(){ + $(this).toggleClass("textrow-selected"); + } ); + + }; + + function freshStyle() { + $("div", "#text_container").removeClass("odd"); + $("div:odd", "#text_container").addClass("odd"); + }; + + $("#txtLock").click(function(){ + if($("#txtLock").is(':checked')) { + $("#txtImpotant").attr("checked",null); + $("#divState").hide(); + } else { + $("#divState").show(); + } + }); + + // 初始化附加提单信息的增删操作 + $("#extra_add").click(function(){ + addTextarea(""); + freshStyle(); + return false; + }); + $("#extra_del").click( function(){ + $("div.textrow-selected", $("#text_container") ).remove(); + freshStyle(); + return false; + } ); + + $.each( EXTRAS, function(i, v){ + addTextarea( v ); + }); + freshStyle(); + +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/wro.xml b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/wro.xml new file mode 100644 index 0000000..92e3572 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-bd-web/src/main/resources/wro.xml @@ -0,0 +1,10 @@ + + + + + classpath:js/bd/utils.js + + + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/pom.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/pom.xml new file mode 100644 index 0000000..aca7367 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + + com.ksa + ksa-web-root + 3.9.0 + + + ksa-finance-web + jar + + ksa-finance-web + 杭州凯思爱物流管理系统 - 财务管理 WEB 模块 + + + UTF-8 + + + + + com.ksa + ksa-finance-service + ${project.version} + + + com.ksa + ksa-logistics-web + ${project.version} + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountAction.java new file mode 100644 index 0000000..153dc7b --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountAction.java @@ -0,0 +1,84 @@ +package com.ksa.web.struts2.action.finance.account; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.struts2.json.JSONException; +import org.apache.struts2.json.JSONUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.model.finance.Account; +import com.ksa.model.finance.FinanceModel; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + + +public class AccountAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = 7722492556451459849L; + private static Logger logger = LoggerFactory.getLogger( AccountAction.class ); + + protected Account account = new Account(); + + protected Map params = new HashMap(); + protected String title; + protected int selected = 0; + + @Override + public String execute() throws Exception { + int direction = account.getDirection(); + if( ! SecurityUtils.isPermitted( "finance:account" + direction ) ) { + return NO_PERMISSION; + } + if( title == null ) { + if ( FinanceModel.isIncome( direction ) ) { + title = "结算单管理"; + } else { + title = "对账单管理"; + } + } + return super.execute(); + } + + @Override + public Account getModel() { + return account; + } + + + public String getParamsMap() { + try { + return JSONUtil.serialize( params ); + } catch( JSONException e ) { + logger.warn( "序列化初始查询参数表发生异常!", e ); + return "{}"; + } + } + + public Map getParams() { + return this.params; + } + + public void setParams( Map queryParams ) { + this.params = queryParams; + } + + public String getTitle() { + return title; + } + + public void setTitle( String title ) { + this.title = title; + } + + public int getSelected() { + return selected; + } + + public void setSelected( int selected ) { + this.selected = selected; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountCodeComputeAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountCodeComputeAction.java new file mode 100644 index 0000000..1dce457 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountCodeComputeAction.java @@ -0,0 +1,55 @@ +package com.ksa.web.struts2.action.finance.account; + +import java.util.Calendar; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.AccountService; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + + +public class AccountCodeComputeAction extends DefaultActionSupport implements JsonAction { + private static final long serialVersionUID = -8547400686834977166L; + private JsonResult result; + // 结算对象编码 + private String code; + + @Override + protected String doExecute() throws Exception { + + if( !StringUtils.hasText( code ) ) { + result = new JsonResult( ERROR, "请输入结算对象的编号。" ); + return SUCCESS; + } + + Calendar calendar = Calendar.getInstance(); + StringBuilder sb = new StringBuilder( 6 + code.length() ); + sb.append( calendar.get( Calendar.YEAR ) % 100 ); + int month = calendar.get( Calendar.MONTH ) + 1; + sb.append( month < 10 ? "0" : "" ); + sb.append( month ); + int day = calendar.get( Calendar.DATE ); + sb.append( day < 10 ? "0" : "" ); + sb.append( day ); + sb.append( code.toUpperCase() ); + String pattenCode = sb.toString(); + int count = ServiceContextUtils.getService( AccountService.class ).querySimilarAccountCodeCount( pattenCode ) + 1; + result = new JsonResult( SUCCESS, "J" + pattenCode + count ); + return super.doExecute(); + } + + public String getCode() { + return code; + } + + public void setCode( String code ) { + this.code = code; + } + + @Override + public Object getJsonResult() { + return result; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDeleteAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDeleteAction.java new file mode 100644 index 0000000..3e10597 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDeleteAction.java @@ -0,0 +1,30 @@ +package com.ksa.web.struts2.action.finance.account; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.AccountService; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + +public class AccountDeleteAction extends AccountAction implements JsonAction { + + private static final long serialVersionUID = -8547400686834977166L; + private JsonResult result; + + @Override + public String doExecute() throws Exception { + AccountService service = ServiceContextUtils.getService( AccountService.class ); + account = service.removeInvoice( account ); + String message = String.format( "成功删除账单:'%s'。", account.getCode() ); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message, account ); + return SUCCESS; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next(), account ); + } + return this.result; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDownloadAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDownloadAction.java new file mode 100644 index 0000000..76f9714 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountDownloadAction.java @@ -0,0 +1,197 @@ +package com.ksa.web.struts2.action.finance.account; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.ksa.model.logistics.BookingNote; +import com.ksa.web.struts2.action.finance.account.map.ValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.AbstractBookingNoteValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.AgentValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CargoContainerValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CargoNameValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CargoQuantityValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CargoVolumnValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CargoWeightValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CodeValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.ConsigneeValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CreatedDateValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CreatorValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CustomerValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CustomsBrokerValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CustomsCodeValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.CustomsDateValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.DepartureDateValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.DeparturePortValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.DestinationDateValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.DestinationPortValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.InvoiceValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.MawbValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.RouteNameValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.RouteValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.SalerValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.ShipperValueGetter; +import com.ksa.web.struts2.action.finance.account.map.bookingnote.TypeValueGetter; + + +public class AccountDownloadAction extends AccountExcelAction { + + private static final long serialVersionUID = 904808495618217566L; + + private static ValueGetter NOTETYPE_GETTER = new TypeValueGetter(); + private static List DEFAULT_SHOW_COLUMNS = new ArrayList(); // Excel 结算单中默认显示的列 + private static Map VALUE_MAPPER = new HashMap(); + static { + DEFAULT_SHOW_COLUMNS.add( "serial_number" ); + DEFAULT_SHOW_COLUMNS.add( "cargo_container" ); + DEFAULT_SHOW_COLUMNS.add( "departure_port" ); + DEFAULT_SHOW_COLUMNS.add( "departure_date" ); + DEFAULT_SHOW_COLUMNS.add( "destination_port" ); + DEFAULT_SHOW_COLUMNS.add( "mawb" ); + DEFAULT_SHOW_COLUMNS.add( "route_name" ); + + VALUE_MAPPER.put( "serial_number", new CodeValueGetter() ); + VALUE_MAPPER.put( "type", new TypeValueGetter() ); + VALUE_MAPPER.put( "created_date", new CreatedDateValueGetter() ); + VALUE_MAPPER.put( "customer_name", new CustomerValueGetter() ); + VALUE_MAPPER.put( "invoice_number", new InvoiceValueGetter() ); + VALUE_MAPPER.put( "creator_name", new CreatorValueGetter() ); + VALUE_MAPPER.put( "saler_name", new SalerValueGetter() ); + VALUE_MAPPER.put( "agent_name", new AgentValueGetter() ); + VALUE_MAPPER.put( "mawb", new MawbValueGetter() ); + VALUE_MAPPER.put( "shipper_name", new ShipperValueGetter() ); + VALUE_MAPPER.put( "consignee_name", new ConsigneeValueGetter() ); + VALUE_MAPPER.put( "cargo_name", new CargoNameValueGetter() ); + VALUE_MAPPER.put( "volumn", new CargoVolumnValueGetter() ); + VALUE_MAPPER.put( "weight", new CargoWeightValueGetter() ); + VALUE_MAPPER.put( "quantity", new CargoQuantityValueGetter() ); + VALUE_MAPPER.put( "cargo_container", new CargoContainerValueGetter() ); + VALUE_MAPPER.put( "customs_broker_name", new CustomsBrokerValueGetter() ); + VALUE_MAPPER.put( "customs_code", new CustomsCodeValueGetter() ); + VALUE_MAPPER.put( "customs_date", new CustomsDateValueGetter() ); + VALUE_MAPPER.put( "route", new RouteValueGetter() ); + VALUE_MAPPER.put( "route_name", new RouteNameValueGetter() ); + VALUE_MAPPER.put( "departure_port", new DeparturePortValueGetter() ); + VALUE_MAPPER.put( "departure_date", new DepartureDateValueGetter() ); + VALUE_MAPPER.put( "destination_port", new DestinationPortValueGetter() ); + VALUE_MAPPER.put( "destination_date", new DestinationDateValueGetter() ); + } + + protected List columns; + protected String filename = "test.xml"; + + protected Set bookingNoteHeader = new LinkedHashSet(); + protected List> bookingNoteData = new ArrayList>(); + + protected Calendar calendar = Calendar.getInstance(); + + @Override + protected String doExecute() throws Exception { + // account.setDirection( -1 ); + String result = super.doExecute(); + + calendar.setTime( account.getCreatedDate() ); + calendar.set( Calendar.DAY_OF_MONTH, 1 ); + // 账期暂定为设置结算单的前一个月 + // calendar.add( Calendar.DAY_OF_MONTH, -1 ); 暂时取消这个"前一个月"的需求 + + filename = ( account.getDirection() == 1 ? "结算单" : "对账单" ) + "-" + account.getCode() + ".xls"; + + BookingNote note = null; + List showColumns = getColumns(); + for( int i = 0; i < bookingNotes.size(); i++ ) { + note = bookingNotes.get( i ); + List values = new ArrayList( showColumns.size() + 1 ); // 第0列放入结算单类型 + values.add( getBookingNoteType( note ) ); + for( String columnName : showColumns ) { + AbstractBookingNoteValueGetter getter = VALUE_MAPPER.get( columnName ); + values.add( getter.getValue( note ) ); + } + bookingNoteData.add( values ); + } + if( note != null ) { + for( String columnName : showColumns ) { + AbstractBookingNoteValueGetter getter = VALUE_MAPPER.get( columnName ); + bookingNoteHeader.add( getter.getLabel( note ) ); + } + } + + return result; + } + + protected String getBookingNoteType( BookingNote note ) { + return NOTETYPE_GETTER.getValue( note ); + } + + @Deprecated + public String getServiceType() { + if( bookingNotes == null || bookingNotes.size() <= 0 ) { + return "其他"; + } + return NOTETYPE_GETTER.getValue( bookingNotes.get( 0 ) ); + } + + // 账期起始日 + public String getAccountBeginDate() { + return calendar.get( Calendar.YEAR ) + "-" + ( calendar.get( Calendar.MONTH ) + 1 ) + "-1"; + } + // 账期截止日 + public String getAccountEndDate() { + return calendar.get( Calendar.YEAR ) + "-" + ( calendar.get( Calendar.MONTH ) + 1 ) + "-" + calendar.get( Calendar.DAY_OF_MONTH ); + } + + // 账单月份 + public String getAccountMonth() { + return calendar.get( Calendar.YEAR ) + "-" + ( calendar.get( Calendar.MONTH ) + 1 ); + } + + public Set getBookingNoteHeader() { + return bookingNoteHeader; + } + + // bookingnote 业务 + public List> getBookingNoteData() { + return bookingNoteData; + } + + public List getColumns() { + if( columns == null || columns.size() == 0 ) { + columns = DEFAULT_SHOW_COLUMNS; + } + return columns; + } + + public void setColumns( List columns ) { + this.columns = columns; + } + + public String getFilename() { + try { + return new String(filename.getBytes("GBK"), "ISO8859-1"); // 修复下载文件名为乱码的bug + } catch( UnsupportedEncodingException e ) { + return filename; + } + } + + public void setFilename( String fileame ) { + this.filename = fileame; + } + + // 2013-10-12 添加按类型分组的功能 + public Map getEmptyTotalCharge() { + Map total = new HashMap(); + for( Set set : chargeHeader.values() ) { + for( String name : set ) { + total.put( name, new Float( 0 ) ); + } + } + return total; + } + + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountEditAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountEditAction.java new file mode 100644 index 0000000..3d99857 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountEditAction.java @@ -0,0 +1,49 @@ +package com.ksa.web.struts2.action.finance.account; + +import org.apache.struts2.json.JSONException; +import org.apache.struts2.json.JSONUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.Account; +import com.ksa.service.finance.AccountService; +import com.ksa.service.security.util.SecurityUtils; + + +public class AccountEditAction extends AccountAction { + + private static final Logger logger = LoggerFactory.getLogger( AccountEditAction.class ); + + private static final long serialVersionUID = -2378526963715464865L; + + + @Override + protected String doExecute() throws Exception { + if( StringUtils.hasText( account.getId() ) ) { + AccountService service = ServiceContextUtils.getService( AccountService.class ); + try { + account = service.loadAccountById( account.getId() ); + } catch( IllegalArgumentException e ) { + logger.warn( "获取结算单发生异常", e ); + account = new Account(); + account.setCreator( SecurityUtils.getCurrentUser() ); + } + } else { + account.setCreator( SecurityUtils.getCurrentUser() ); + } + return SUCCESS; + } + + public String getJsonCharges() { + try { + return JSONUtil.serialize( account.getCharges(), null, null, false, true ); + } catch( JSONException e ) { + logger.warn( "序列化数据列表发生异常!", e ); + return "[]"; + } + } + + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountExcelAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountExcelAction.java new file mode 100644 index 0000000..86949c6 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountExcelAction.java @@ -0,0 +1,228 @@ +package com.ksa.web.struts2.action.finance.account; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.struts2.json.JSONException; +import org.apache.struts2.json.JSONUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.service.finance.AccountService; + + +public class AccountExcelAction extends AccountAction { + + private static final long serialVersionUID = -2968592667293828024L; + + private static final Logger logger = LoggerFactory.getLogger( AccountExcelAction.class ); + + protected static final Map TYPE_SORTED_MAP; + static { + TYPE_SORTED_MAP = new HashMap(); + TYPE_SORTED_MAP.put( BookingNote.TYPE_SEA_EXPORT, 0 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_SEA_IMPORT, 1 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_AIR_EXPORT, 2 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_AIR_IMPORT, 3 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_NATIVE, 4 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_KB, 5 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_CC, 6 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_BC, 7 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_RH, 8 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_TL, 9 ); + TYPE_SORTED_MAP.put( BookingNote.TYPE_ZJ, 10 ); + } + + protected List bookingNotes; + protected List rates; + + // key : currency.name, value : type's set + protected Map> chargeHeader = new LinkedHashMap>(); + protected Map> chargeData = new LinkedHashMap>(); + protected Map chargeRate = new LinkedHashMap(); + protected Float totalSum = 0f; + + @SuppressWarnings( "unchecked" ) + @Override + protected String doExecute() throws Exception { + AccountService service = ServiceContextUtils.getService( AccountService.class ); + account = service.loadAccountById( account.getId() ); + bookingNotes = service.loadAccountBookingNotes( account ); + + // 对业务进行排序 + Collections.sort( bookingNotes, new Comparator() { + + @Override + public int compare( BookingNote b1, BookingNote b2 ) { + String t1 = b1.getType(); + String t2 = b2.getType(); + if( t1.equals( t2 ) ) { + return b1.getSerialNumber() - b2.getSerialNumber(); + } else { + return TYPE_SORTED_MAP.get( t1 ).compareTo( TYPE_SORTED_MAP.get( t2 ) ); + } + } + } ); + + rates = (List) service.loadAccountCurrencyRates( account ); + + List charges = account.getCharges(); + Collections.sort( charges, new Comparator() { + @Override + public int compare( Charge c1, Charge c2 ) { // 按币种排序 + int rank1 = c1.getCurrency().getRank(); + int rank2 = c2.getCurrency().getRank(); + if( rank1 > rank2 ) { + return 1; + } else if( rank1 < rank2 ) { + return -1; + } + return 0; + } + } ); + + // 处理汇率map + for( CurrencyRate rate : rates ) { + String name = rate.getCurrency().getName(); + chargeRate.put( name, rate.getRate() ); + } + + for( Charge c : account.getCharges() ) { + String name = c.getCurrency().getName(); + if( chargeHeader.containsKey( name ) ) { + chargeHeader.get( name ).add( getDataFieldId( name, c.getType() ) ); + } else { + Set chargeTypes = new LinkedHashSet(); + chargeTypes.add( getDataFieldId( name, c.getType() ) ); + chargeHeader.put( name, chargeTypes ); + } + } + + for( String name : chargeHeader.keySet() ) { + chargeHeader.get( name ).add( getAggregateFieldId( name ) ); + } + + // 准备数据 + for(BookingNote bn : bookingNotes ) { + Map data = new LinkedHashMap(); + chargeData.put( bn.getId(), data ); + for( String name : chargeHeader.keySet() ) { + data.put( getAggregateFieldId( name ), new Float( 0 ) ); + } + } + + Map total = new LinkedHashMap(); + chargeData.put( "TOTAL", total ); // 加入最后的统计行 + for( Set set : chargeHeader.values() ) { + for( String name : set ) { + total.put( name, new Float( 0 ) ); + } + } + + for( Charge c : account.getCharges() ) { + String name = c.getCurrency().getName(); + Map data = chargeData.get( c.getBookingNote().getId() ); + String fieldId = getDataFieldId( name, c.getType() ); + data.put( fieldId, c.getAmount() ); + String sumField = getAggregateFieldId( name ); + data.put( sumField, data.get( sumField ) + c.getAmount() ); + + total.put( fieldId, c.getAmount() + total.get( fieldId ) ); + total.put( sumField, c.getAmount() + total.get( sumField ) ); + totalSum += c.getAmount() * chargeRate.get( name ); // 按汇率折算 + } + + return SUCCESS; + } + + private String getAggregateFieldId( String currencyName ) { + return currencyName + "-小计"; + } + private String getDataFieldId( String currencyName, String chargeType ) { + return currencyName + "-" + chargeType; + } + + public String getJsonBookingNotes() { + try { + return JSONUtil.serialize( bookingNotes, null, null, false, true ); + } catch( JSONException e ) { + logger.warn( "序列化数据列表发生异常!", e ); + return "[]"; + } + } + + public List getBookingNotes() { + return bookingNotes; + } + + public Map> getChargeHeader() { + return chargeHeader; + } + + public Map> getChargeData() { + return chargeData; + } + + public Map getChargeRate() { + return chargeRate; + } + + public Float getTotal() { + return totalSum; + } + + public static class Pair implements Serializable { + + private static final long serialVersionUID = 5310839690899689547L; + + public Pair( K key, V value ) { + this.key = key; + this.value = value; + } + + protected K key; + protected V value; + + public K getKey() { + return key; + } + + public void setKey( K key ) { + this.key = key; + } + + public V getValue() { + return value; + } + + public void setValue( V value ) { + this.value = value; + } + + @Override + public boolean equals( Object obj ) { + if( obj == null || ! (obj instanceof Pair ) ) { + return false; + } + Pair temp = ( Pair ) obj; + return key.equals( temp.getKey() ) && value.equals( temp.getValue() ); + } + + @Override + public int hashCode() { + return 7 * key.hashCode() + 23 * value.hashCode(); + } + + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountQueryAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountQueryAction.java new file mode 100644 index 0000000..a39545f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountQueryAction.java @@ -0,0 +1,95 @@ +package com.ksa.web.struts2.action.finance.account; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.ksa.dao.DateQueryClause; +import com.ksa.dao.QueryClause; +import com.ksa.dao.TextQueryClause; +import com.ksa.web.struts2.action.finance.query.AccountStateQueryClause; +import com.ksa.web.struts2.action.logistics.bookingnote.BookingNoteQueryAction; +import com.opensymphony.xwork2.ActionContext; + +public class AccountQueryAction extends BookingNoteQueryAction { + + private static final long serialVersionUID = 3940356902474565176L; + + private static final String QUERY_CLAUSE_KEY_PREFIX = "ACCOUNT_"; + private static final String DIRECTION_QUERY_PARAMETER = "direction"; + private static final String NATURE_QUERY_PARAMETER = "nature"; + + protected static final Map accountQueryClauses = new HashMap( 32 ); + + static { + for( String key : preparedQueryClauses.keySet() ) { + accountQueryClauses.put( key, preparedQueryClauses.get( key ) ); + } + accountQueryClauses.put( "ACCOUNT_STATE", new AccountStateQueryClause() ); + accountQueryClauses.put( "ACCOUNT_CODE", new TextQueryClause( "a.CODE" ) ); + accountQueryClauses.put( "ACCOUNT_TARGET", new TextQueryClause( "a.TARGET_ID" ) ); + accountQueryClauses.put( "ACCOUNT_DATE", new DateQueryClause( "a.CREATED_DATE" ) ); + accountQueryClauses.put( "ACCOUNT_DEADLINE", new DateQueryClause( "a.DEADLINE" ) ); + accountQueryClauses.put( "ACCOUNT_PAYMENT", new DateQueryClause( "a.PAYMENT_DATE" ) ); + accountQueryClauses.put( "ACCOUNT_INPUTOR", new TextQueryClause( "a.CREATOR_ID" ) ); + } + + @Override + protected Map getParameters() { + ActionContext context = ActionContext.getContext(); + Map paras = context.getParameters(); + Collection noteClauseList = new ArrayList( 16 ); // 针对 bookingnote 表所做的查询 + Collection accountClauseList = new ArrayList( 16 ); + + Set keys = paras.keySet(); + for( String key : keys ) { + String queryKey = key.toUpperCase(); + if( accountQueryClauses.containsKey( queryKey ) ) { + QueryClause qc = accountQueryClauses.get( queryKey ); + Object value = paras.get( key ); + if( value.getClass().isArray() ) { + Collection clauses = qc.compute( ( String[] ) value ); + if( clauses != null && clauses.size() > 0 ) { + if( queryKey.startsWith( QUERY_CLAUSE_KEY_PREFIX ) ) { + accountClauseList.addAll( clauses ); + } else { + noteClauseList.addAll( clauses ); + } + } + } + } else if( DIRECTION_QUERY_PARAMETER.equalsIgnoreCase( key ) ) { + Object value = paras.get( key ); + if( value.getClass().isArray() ) { + paras.put( DIRECTION_QUERY_PARAMETER, Array.get( value, 0 ) ); + } + } else if( NATURE_QUERY_PARAMETER.equalsIgnoreCase( key ) ) { + Object value = paras.get( key ); + if( value.getClass().isArray() ) { + paras.put( NATURE_QUERY_PARAMETER, Array.get( value, 0 ) ); + } + } + } + if( noteClauseList.size() > 0 ) { + paras.put( "noteQueryClauses", noteClauseList.toArray() ); + } + if( accountClauseList.size() > 0 ) { + paras.put( "queryClauses", accountClauseList.toArray() ); + } + + return paras; + } + + @Override + protected String getQueryCountStatement() { + return "count-finance-account-query"; + } + + @Override + protected String getQueryDataStatement() { + return "grid-finance-account-query"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountRateGridDataAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountRateGridDataAction.java new file mode 100644 index 0000000..033532d --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountRateGridDataAction.java @@ -0,0 +1,39 @@ +package com.ksa.web.struts2.action.finance.account; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.Account; +import com.ksa.service.finance.AccountService; +import com.ksa.web.struts2.action.data.GridDataActionSupport; +import com.opensymphony.xwork2.ModelDriven; + + +public class AccountRateGridDataAction extends GridDataActionSupport implements ModelDriven { + + private static final long serialVersionUID = -7173546634356708920L; + + protected Account account = new Account(); + protected Object[] gridDataArray = new Object[0]; + + @Override + protected String doExecute() throws Exception { + AccountService service = ServiceContextUtils.getService( AccountService.class ); + gridDataArray = service.loadAccountCurrencyRates( account ).toArray(); + return SUCCESS; + } + + @Override + protected Object[] queryGridData() { + return gridDataArray; + } + + @Override + protected int queryGridDataCount() { + return gridDataArray.length; + } + + @Override + public Account getModel() { + return account; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountSaveAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountSaveAction.java new file mode 100644 index 0000000..31af202 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountSaveAction.java @@ -0,0 +1,38 @@ +package com.ksa.web.struts2.action.finance.account; + +import java.util.List; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.AccountCurrencyRate; +import com.ksa.service.finance.AccountService; +import com.ksa.util.StringUtils; + + +public class AccountSaveAction extends AccountEditAction { + + private static final long serialVersionUID = -1519773982733347627L; + + /** 结算单对应的汇率 */ + protected List rates; + + @Override + protected String doExecute() throws Exception { + AccountService service = ServiceContextUtils.getService( AccountService.class ); + + boolean isNew = ! StringUtils.hasText( account.getId() ); + account = service.saveAccount( account, rates ); + if( isNew ) { + return INPUT; + } + return SUCCESS; + } + + public List getRates() { + return rates; + } + + public void setRates( List rates ) { + this.rates = rates; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountStateAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountStateAction.java new file mode 100644 index 0000000..3730b69 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/AccountStateAction.java @@ -0,0 +1,72 @@ +package com.ksa.web.struts2.action.finance.account; + +import java.util.Date; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.AccountState; +import com.ksa.service.finance.AccountService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + + +public class AccountStateAction extends AccountAction implements JsonAction { + + private static final long serialVersionUID = -1301276137468934314L; + + private JsonResult result; + + @Override + public String execute() throws Exception { + int state = account.getState(); + if( AccountState.isChecked( state ) ) { + if( ! SecurityUtils.isPermitted( "finance:account-check" ) ) { + result = new JsonResult( ERROR, "对不起,您没有权限进行结算/对账单审核操作!" ); + return ERROR; + } + } else if( AccountState.isSettled( state ) ) { + if( ! SecurityUtils.isPermitted( "finance:account-settle" ) ) { + result = new JsonResult( ERROR, "对不起,您没有权限进行结算/对账单结算完毕操作!" ); + return ERROR; + } + } + return super.execute(); + } + + @Override + public String doExecute() throws Exception { + AccountService service = ServiceContextUtils.getService( AccountService.class ); + + // 结算完毕状态,则必须首先保存结算完毕日期 + if( AccountState.isSettled( account.getState() ) ) { + Account temp = service.loadAccountById( account.getId() ); + if( temp == null ) { + result = new JsonResult( SUCCESS, "%s不存在,请确认提交数据的正确性。",( account.getDirection() == 1 ? "结算单" : "对账单" ) ); + } + + if( temp.getPaymentDate() == null ) { + if( account.getPaymentDate() != null ) { + temp.setPaymentDate( account.getPaymentDate() ); + } else { + temp.setPaymentDate( new Date() ); + } + service.saveAccount( temp, null ); + } + } + + account = service.updateAccountState( account ); + String message = String.format( "成功更新%s '%s' 的状态。",( account.getDirection() == 1 ? "结算单" : "对账单" ), account.getCode() ); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message ); + return SUCCESS; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next() ); + } + return this.result; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/ValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/ValueGetter.java new file mode 100644 index 0000000..e2bb50a --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/ValueGetter.java @@ -0,0 +1,9 @@ +package com.ksa.web.struts2.action.finance.account.map; + + +public interface ValueGetter { + /** 获取对象值 */ + String getValue( T obj ); + /** 获取对象值的说明 */ + String getLabel( T obj ); +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AbstractBookingNoteValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AbstractBookingNoteValueGetter.java new file mode 100644 index 0000000..1eb10ad --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AbstractBookingNoteValueGetter.java @@ -0,0 +1,27 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import com.ksa.model.logistics.BookingNote; +import com.ksa.web.struts2.action.finance.account.map.ValueGetter; + +public abstract class AbstractBookingNoteValueGetter implements ValueGetter { + + protected DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + + @Override + public String getValue( BookingNote obj ) { + if( obj == null ) { + return ""; + } else { + try { + return doGetValue( obj ); + } catch( Exception e ) { + return ""; + } + } + } + + protected abstract String doGetValue( BookingNote obj ); +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AgentValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AgentValueGetter.java new file mode 100644 index 0000000..567fc0e --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/AgentValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class AgentValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getAgent() == null ) { + return ""; + } + return obj.getAgent().getName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "代理商"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoContainerValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoContainerValueGetter.java new file mode 100644 index 0000000..db950f5 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoContainerValueGetter.java @@ -0,0 +1,18 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CargoContainerValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getCargoContainer(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "箱类箱量"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoNameValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoNameValueGetter.java new file mode 100644 index 0000000..039cb9e --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoNameValueGetter.java @@ -0,0 +1,18 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CargoNameValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getCargoName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "品名"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoQuantityValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoQuantityValueGetter.java new file mode 100644 index 0000000..be8f310 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoQuantityValueGetter.java @@ -0,0 +1,29 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CargoQuantityValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getQuantity() == null ) { + return ""; + } + if( obj.getQuantity() > 0 ) { + StringBuilder sb = new StringBuilder(); + sb.append( obj.getQuantity() ); + if( obj.getUnit() != null ) { + sb.append( " " ).append( obj.getUnit() ); + } + return sb.toString(); + } + return ""; + } + + @Override + public String getLabel( BookingNote obj ) { + return "数量"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoVolumnValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoVolumnValueGetter.java new file mode 100644 index 0000000..7a5263a --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoVolumnValueGetter.java @@ -0,0 +1,24 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CargoVolumnValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getVolumn() == null ) { + return ""; + } + if( obj.getVolumn() > 0 ) { + return obj.getVolumn().toString() + " m3"; + } + return ""; + } + + @Override + public String getLabel( BookingNote obj ) { + return "体积"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoWeightValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoWeightValueGetter.java new file mode 100644 index 0000000..0231d53 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CargoWeightValueGetter.java @@ -0,0 +1,24 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CargoWeightValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getWeight() == null ) { + return ""; + } + if( obj.getWeight() > 0 ) { + return obj.getWeight().toString() + " kg"; + } + return ""; + } + + @Override + public String getLabel( BookingNote obj ) { + return "毛重"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CodeValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CodeValueGetter.java new file mode 100644 index 0000000..144cd83 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CodeValueGetter.java @@ -0,0 +1,18 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CodeValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getCode(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "业务编号"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ConsigneeValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ConsigneeValueGetter.java new file mode 100644 index 0000000..14d8340 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ConsigneeValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class ConsigneeValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getConsignee() == null ) { + return ""; + } + return obj.getConsignee().getName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "收货人"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatedDateValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatedDateValueGetter.java new file mode 100644 index 0000000..b20fbac --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatedDateValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CreatedDateValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getCreatedDate() == null ) { + return ""; + } + return DATE_FORMAT.format( obj.getCreatedDate() ); + } + + @Override + public String getLabel( BookingNote obj ) { + return "接单日期"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatorValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatorValueGetter.java new file mode 100644 index 0000000..58fb09e --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CreatorValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CreatorValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getCreator() == null ) { + return ""; + } + return obj.getCreator().getName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "操作员"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomerValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomerValueGetter.java new file mode 100644 index 0000000..e6e5d75 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomerValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CustomerValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getCustomer() == null ) { + return ""; + } + return obj.getCustomer().getName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "客户"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsBrokerValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsBrokerValueGetter.java new file mode 100644 index 0000000..0499ce0 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsBrokerValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CustomsBrokerValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getCustomsBroker() == null ) { + return ""; + } + return obj.getCustomsBroker().getName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "报关行"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsCodeValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsCodeValueGetter.java new file mode 100644 index 0000000..170999d --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsCodeValueGetter.java @@ -0,0 +1,18 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CustomsCodeValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getCustomsCode(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "报关单号"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsDateValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsDateValueGetter.java new file mode 100644 index 0000000..f13204b --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/CustomsDateValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class CustomsDateValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getCustomsDate() == null ) { + return ""; + } + return DATE_FORMAT.format( obj.getCustomsDate() ); + } + + @Override + public String getLabel( BookingNote obj ) { + return "报关日期"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DepartureDateValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DepartureDateValueGetter.java new file mode 100644 index 0000000..6fbbb8c --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DepartureDateValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class DepartureDateValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getDepartureDate() == null ) { + return ""; + } + return DATE_FORMAT.format( obj.getDepartureDate() ); + } + + @Override + public String getLabel( BookingNote obj ) { + return "开航日"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DeparturePortValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DeparturePortValueGetter.java new file mode 100644 index 0000000..dfb6ab8 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DeparturePortValueGetter.java @@ -0,0 +1,23 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class DeparturePortValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getDeparturePort(); + } + + @Override + public String getLabel( BookingNote obj ) { + String type = obj.getType(); + if( BookingNote.TYPE_SEA_EXPORT.equalsIgnoreCase( type ) || BookingNote.TYPE_SEA_IMPORT.equalsIgnoreCase( type ) ) { + return "起运港"; + } else { + return "起飞港"; + } + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationDateValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationDateValueGetter.java new file mode 100644 index 0000000..ce7f440 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationDateValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class DestinationDateValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getDestinationDate() == null ) { + return ""; + } + return DATE_FORMAT.format( obj.getDestinationDate() ); + } + + @Override + public String getLabel( BookingNote obj ) { + return "到港日"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationPortValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationPortValueGetter.java new file mode 100644 index 0000000..c1069a2 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/DestinationPortValueGetter.java @@ -0,0 +1,24 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class DestinationPortValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getDestinationPort(); + } + + @Override + public String getLabel( BookingNote obj ) { + /*String type = obj.getType(); + if( BookingNote.TYPE_SEA_EXPORT.equalsIgnoreCase( type ) || BookingNote.TYPE_SEA_IMPORT.equalsIgnoreCase( type ) ) { + return "目的港"; + } else { + return "起飞港"; + }*/ + return "目的港"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/InvoiceValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/InvoiceValueGetter.java new file mode 100644 index 0000000..8fcd21f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/InvoiceValueGetter.java @@ -0,0 +1,18 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class InvoiceValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getInvoiceNumber(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "发票号"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/MawbValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/MawbValueGetter.java new file mode 100644 index 0000000..6a73bca --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/MawbValueGetter.java @@ -0,0 +1,33 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class MawbValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( BookingNote.TYPE_SEA_EXPORT.equalsIgnoreCase( obj.getType() ) ) { + // 海运出口提单仅统计分单号 + if( obj.getHawb() != null ) { + return obj.getHawb(); + } + return ""; + } + + StringBuilder sb = new StringBuilder(); + if( obj.getMawb() != null ) { + sb.append( obj.getMawb() ).append( " " ); + } + if( obj.getHawb() != null ) { + sb.append( obj.getHawb() ); + } + return sb.toString(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "提单号"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteNameValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteNameValueGetter.java new file mode 100644 index 0000000..a222f1d --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteNameValueGetter.java @@ -0,0 +1,30 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class RouteNameValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + StringBuilder sb = new StringBuilder(); + if( obj.getRouteName() != null ) { + sb.append( obj.getRouteName() ).append( " " ); + } + if( obj.getRouteCode() != null ) { + sb.append( obj.getRouteCode() ); + } + return sb.toString(); + } + + @Override + public String getLabel( BookingNote obj ) { + String type = obj.getType(); + if( BookingNote.TYPE_SEA_EXPORT.equalsIgnoreCase( type ) || BookingNote.TYPE_SEA_IMPORT.equalsIgnoreCase( type ) ) { + return "船名/航次"; + } else { + return "航班"; + } + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteValueGetter.java new file mode 100644 index 0000000..28dbec5 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/RouteValueGetter.java @@ -0,0 +1,18 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class RouteValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + return obj.getRoute(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "航线"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/SalerValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/SalerValueGetter.java new file mode 100644 index 0000000..cf85909 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/SalerValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class SalerValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getSaler() == null ) { + return ""; + } + return obj.getSaler().getName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "销售担当"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ShipperValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ShipperValueGetter.java new file mode 100644 index 0000000..b6d0f48 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/ShipperValueGetter.java @@ -0,0 +1,21 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class ShipperValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj.getShipper() == null ) { + return ""; + } + return obj.getShipper().getName(); + } + + @Override + public String getLabel( BookingNote obj ) { + return "发货人"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/TypeValueGetter.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/TypeValueGetter.java new file mode 100644 index 0000000..760dff0 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/account/map/bookingnote/TypeValueGetter.java @@ -0,0 +1,44 @@ +package com.ksa.web.struts2.action.finance.account.map.bookingnote; + +import com.ksa.model.logistics.BookingNote; + + +public class TypeValueGetter extends AbstractBookingNoteValueGetter { + + @Override + public String doGetValue( BookingNote obj ) { + if( obj != null ) { + String type = obj.getType(); + if( BookingNote.TYPE_SEA_EXPORT.equalsIgnoreCase( type ) ) { + return "海运出口"; + } else if( BookingNote.TYPE_SEA_IMPORT.equalsIgnoreCase( type ) ) { + return "海运进口"; + } else if( BookingNote.TYPE_AIR_EXPORT.equalsIgnoreCase( type ) ) { + return "空运出口"; + } else if( BookingNote.TYPE_AIR_IMPORT.equalsIgnoreCase( type ) ) { + return "空运进口"; + } else if( BookingNote.TYPE_NATIVE.equalsIgnoreCase( type ) ) { + return "国内运输"; + } else if( BookingNote.TYPE_KB.equalsIgnoreCase( type ) ) { + return "捆包业务"; + } else if( BookingNote.TYPE_BC.equalsIgnoreCase( type ) ) { + return "搬场业务"; + } else if( BookingNote.TYPE_CC.equalsIgnoreCase( type ) ) { + return "仓储业务"; + } else if( BookingNote.TYPE_RH.equalsIgnoreCase( type ) ) { + return "内航线"; + } else if( BookingNote.TYPE_TL.equalsIgnoreCase( type ) ) { + return "公铁联运"; + } else if( BookingNote.TYPE_ZJ.equalsIgnoreCase( type ) ) { + return "证件代办"; + } + } + return "其他"; + } + + @Override + public String getLabel( BookingNote obj ) { + return "业务类型"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/DebitNoteAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/DebitNoteAction.java new file mode 100644 index 0000000..e1c7f60 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/DebitNoteAction.java @@ -0,0 +1,101 @@ +package com.ksa.web.struts2.action.finance.business; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.business.DebitNoteCharge; +import com.ksa.model.finance.Charge; +import com.ksa.model.finance.FinanceModel; +import com.ksa.model.logistics.BookingNote; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.logistics.BookingNoteService; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + +public class DebitNoteAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = 8576498803387130735L; + + private static int INIT_ARRAY_LENGTH = 12; + + protected BookingNote bookingNote = new BookingNote(); + protected DebitNoteCharge[] charge; + protected Float total = 0f; + + @Override + protected String doExecute() throws Exception { + // 首先获取托单对象 和 托单的费用清单 + BookingNoteService service = ServiceContextUtils.getService( BookingNoteService.class ); + bookingNote = service.loadBookingNoteById( bookingNote.getId() ); + + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + List foreignIncomes = new ArrayList(); + List foreignExpense = null; //new ArrayList(); + + // DEBIT NOTE 仅针对收入费用 + /*List noteCharges = */chargeService.loadBookingNoteCharges( bookingNote.getId(), foreignIncomes, foreignExpense, FinanceModel.FOREIGN ); + /*CurrencyRateService rateService = ServiceContextUtils.getService( CurrencyRateService.class ); + CurrencyRate usdCurrencyRate = rateService.loadLatestCurrencyRate( Currency.USD.getId() ); + total.setRate( usdCurrencyRate.getRate() );*/ + + if( foreignIncomes.size() > 0 ) { + initCharge( foreignIncomes.size() > INIT_ARRAY_LENGTH ? foreignIncomes.size() : INIT_ARRAY_LENGTH ); + for( int i = 0; i < foreignIncomes.size(); i++ ) { + charge[i] = new DebitNoteCharge( foreignIncomes.get( i ) ); + total += ( charge[i].getAmount() != null ? charge[i].getAmount() : 0f ); + } + } else { + initCharge( INIT_ARRAY_LENGTH ); + } + + return SUCCESS; + } + + private void initCharge( int length ) { + charge = new DebitNoteCharge[ length ]; + for( int i = 0; i < length; i++ ) { + charge[i] = new DebitNoteCharge(); + } + } + + public DebitNoteCharge[] getCharge() { + return charge; + } + + public void setCharge( DebitNoteCharge[] charge ) { + this.charge = charge; + } + + public Float getTotal() { + return total; + } + + public void setTotal( Float total ) { + this.total = total; + } + + @Override + public BookingNote getModel() { + return bookingNote; + } + + public BookingNote getBookingNote() { + return bookingNote; + } + + public String getFilename() { + String filename = "DEBIT NOTE"; + if( bookingNote != null ) { + filename = filename + " - " + bookingNote.getCode(); + } + filename += ".xls"; + try { + return new String(filename.getBytes("GBK"), "ISO8859-1"); // 修复下载文件名为乱码的bug + } catch( UnsupportedEncodingException e ) { + return filename; + } + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/RecordBillAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/RecordBillAction.java new file mode 100644 index 0000000..06a88f5 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/business/RecordBillAction.java @@ -0,0 +1,233 @@ +package com.ksa.web.struts2.action.finance.business; + +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.bd.Currency; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.business.RecordBillCharge; +import com.ksa.model.business.RecordBillChargeGather; +import com.ksa.model.business.RecordBillProfit; +import com.ksa.model.finance.Charge; +import com.ksa.model.finance.FinanceModel; +import com.ksa.model.logistics.BookingNote; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.logistics.BookingNoteService; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + + +public class RecordBillAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = -7802092150934083971L; + + private static int INIT_ARRAY_LENGTH = 20; + + protected BookingNote bookingNote = new BookingNote(); + protected RecordBillCharge[] charge; + protected int chargeExtraCount = 0; + protected RecordBillProfit total = new RecordBillProfit(); + protected Map gatherMap = new HashMap(); + + @Override + protected String doExecute() throws Exception { + // 首先获取托单对象 和 托单的费用清单 + BookingNoteService noteService = ServiceContextUtils.getService( BookingNoteService.class ); + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + CurrencyRateService rateService = ServiceContextUtils.getService( CurrencyRateService.class ); + + bookingNote = noteService.loadBookingNoteById( bookingNote.getId() ); + List noteCharges = chargeService.loadBookingNoteCharges( bookingNote.getId() ); + + if( noteCharges != null && noteCharges.size() > 0 ) { + Map incomeMap = new LinkedHashMap(); + Map expenseMap = new LinkedHashMap(); + Set chargeTypeSet = new LinkedHashSet(); + for( Charge c : noteCharges ) { + String key = c.getType(); + if( !chargeTypeSet.contains( key ) ) { + chargeTypeSet.add( key ); + } + Map map = c.getDirection() == FinanceModel.INCOME ? incomeMap : expenseMap; + if( !map.containsKey( key ) ) { + map.put( key, c ); + } else { + // 已经存在同类型的费用,则费用总和相加 + Charge mappedCharge = map.get( key ); + mappedCharge.setAmount( c.getAmount() + mappedCharge.getAmount() ); + // FIXME 同费用类型的单价和数量没有处理 + } + } + + // 生成最后的费用清单 + initCharge( chargeTypeSet.size() > INIT_ARRAY_LENGTH ? chargeTypeSet.size() : INIT_ARRAY_LENGTH ); + int index = 0; + for( String type : chargeTypeSet ) { + RecordBillCharge rbCharge = charge[index]; + if( incomeMap.containsKey( type ) ) { + Charge income = incomeMap.get( type ); + Currency incomeCurrency = income.getCurrency(); + + rbCharge.setCustomer( income.getTarget().getName() ); + rbCharge.setIncome( income.getPrice() ); + rbCharge.setIncomeAmount( income.getQuantity() ); + rbCharge.setIncomeCurrency( incomeCurrency ); + rbCharge.setIncomeTotal( computeMoneyAndAggregateProfit( income ) ); + rbCharge.setType( income.getType() ); + + // 计算币种汇总 + RecordBillChargeGather gather = computeGather( incomeCurrency, rateService ); + if( gather != null ) { + gather.addIncome( income.getAmount() ); + } + + } + if( expenseMap.containsKey( type ) ) { + Charge expense = expenseMap.get( type ); + Currency expenseCurrency = expense.getCurrency(); + + rbCharge.setAgent( expense.getTarget().getName() ); + rbCharge.setExpense( expense.getPrice() ); + rbCharge.setExpenseAmount( expense.getQuantity() ); + rbCharge.setExpenseCurrency( expense.getCurrency() ); + rbCharge.setExpenseTotal( computeMoneyAndAggregateProfit( expense ) ); + rbCharge.setType( expense.getType() ); + + // 计算币种汇总 + RecordBillChargeGather gather = computeGather( expenseCurrency, rateService ); + if( gather != null ) { + gather.addExpense( expense.getAmount() ); + } + } + + index++; + } + + total.setGathers( gatherMap.values() ); + + /*total.setIncome( total.getIncomeRMB() + total.getIncomeUSD() * total.getRate() + total.getIncomeOTHER() ); + total.setExpense( total.getExpenseRMB() + total.getExpenseUSD() * total.getRate() + total.getExpenseOTHER() ); + total.setProfit( total.getIncome() - total.getExpense() ); + if( total.getExpense() > 0.1f ) { + total.setProfitRate( total.getProfit() / total.getIncome() ); // 由 利润/支出合计 改为 利润/收入合计 + }*/ + + } else { + initCharge( INIT_ARRAY_LENGTH ); + } + + // 将charge排序 + // Arrays.sort( charge ); + + return SUCCESS + bookingNote.getType(); + } + + private String computeMoneyAndAggregateProfit( Charge c ) { + String money = Float.toString( c.getAmount() ); + Currency currency = c.getCurrency(); + if( Currency.RMB.getId().equals( currency.getId() ) ) { + money = "¥" + money; + if( c.getDirection() == FinanceModel.INCOME ) { + total.setIncomeRMB( total.getIncomeRMB() + c.getAmount() ); + } else { + total.setExpenseRMB( total.getExpenseRMB() + c.getAmount() ); + } + } else if( Currency.USD.getId().equals( currency.getId() ) ) { + money = "$" + money; + } else { + money = currency.getName() + " " + money; + } + return money; + } + + private void initCharge( int length ) { + charge = new RecordBillCharge[ length ]; + for( int i = 0; i < length; i++ ) { + charge[i] = new RecordBillCharge(); + } + if( length > INIT_ARRAY_LENGTH ) { + chargeExtraCount = length - INIT_ARRAY_LENGTH; + } + } + + @Override + public BookingNote getModel() { + return bookingNote; + } + + public BookingNote getBookingNote() { + return bookingNote; + } + + public RecordBillCharge[] getCharge() { + return charge; + } + + public int getChargeExtraCount() { + return chargeExtraCount; + } + + public RecordBillProfit getTotal() { + return total; + } + + public String getFilename() { + String filename = "作业记录书"; + if( bookingNote != null ) { + filename = filename + " - " + bookingNote.getCode(); + } + filename += ".xls"; + try { + return new String(filename.getBytes("GBK"), "ISO8859-1"); // 修复下载文件名为乱码的bug + } catch( UnsupportedEncodingException e ) { + return filename; + } + } + + private RecordBillChargeGather computeGather( Currency currency, CurrencyRateService rateService ) { + if( currency.getId().equals( Currency.RMB.getId() ) ) { + return null; + } + if( gatherMap.containsKey( currency.getId() ) ) { + return gatherMap.get( currency.getId() ); + } else { + RecordBillChargeGather gather = new RecordBillChargeGather(); + CurrencyRate currencyRate = rateService.loadLatestCurrencyRate( currency.getId() ); + gather.setCurrency( currency ); + gather.setRate( currencyRate.getRate() ); + gatherMap.put( currency.getId(), gather ); + return gather; + } + } + + // 计算除人民币外第二类币种,如果有美元则按美元计算, + // 如果没有美元,且除人民币外只有另外一种外币,则选择此外币。 + /*private Currency computeSecondaryCurrency( List charges ) { + Map otherCurrencyMap = new HashMap(); + for( Charge charge : charges ) { + String cId = charge.getCurrency().getId(); + if( Currency.RMB.getId().endsWith( cId ) ) { + continue; + } else if( Currency.USD.getId().endsWith( cId ) ) { + return Currency.USD; // 有美元则按美元计算 + } else { + // 第三类货币 + otherCurrencyMap.put( cId, charge.getCurrency() ); + } + } + + if( otherCurrencyMap.size() == 1 ) { // 没有美元,且只有一种第三类外币 + return otherCurrencyMap.values().iterator().next(); + } else { // 多种外币,则还是选用美元 + return Currency.USD; + } + }*/ + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeAction.java new file mode 100644 index 0000000..b0e406e --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeAction.java @@ -0,0 +1,155 @@ +package com.ksa.web.struts2.action.finance.charge; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.struts2.json.JSONException; +import org.apache.struts2.json.JSONUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + + +public class ChargeAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = -1007892464889907411L; + + private static Logger logger = LoggerFactory.getLogger( ChargeAction.class ); + private static final Collection excludeProperties = new ArrayList( 8 ); + static { + //excludeProperties.add( Pattern.compile( ".*account.*" ) ); + excludeProperties.add( Pattern.compile( ".*bookingNote.*" ) ); + excludeProperties.add( Pattern.compile( ".*partner.*" ) ); + } + + protected String template; + protected BookingNote bookingNote = new BookingNote(); + protected List incomes = new ArrayList(); + protected List expenses = new ArrayList(); + protected List rates; + protected int nature = 1; + + @Override + public String execute() throws Exception { + if( ! SecurityUtils.isPermitted( "finance:charge" ) ) { + return NO_PERMISSION; + } + return super.execute(); + } + + /*** + * 将费用列表序列化为 JSON 字符串。 + */ + protected String serializeList( List list ) { + try { + return JSONUtil.serialize( list, excludeProperties, null, false, true ); + } catch( JSONException e ) { + logger.warn( "序列化数据列表发生异常!", e ); + return "[]"; + } + } + + /** 获取记账日期 */ + protected Date computeChargeDate() { + // 以第一笔资金的记录时间为记账日期 + if( incomes.size() <= 0 && expenses.size() <=0 ) { + return null; + } + + Date chargeDate = new Date(); + for( Charge charge : incomes ) { + if( charge.getCreatedDate().before( chargeDate ) ) { + chargeDate = charge.getCreatedDate(); + } + } + for( Charge charge : expenses ) { + if( charge.getCreatedDate().before( chargeDate ) ) { + chargeDate = charge.getCreatedDate(); + } + } + return chargeDate; + } + + @Override + public BookingNote getModel() { + return bookingNote; + } + + public List getIncomes() { + return incomes; + } + + public void setIncomes( List incomes ) { + this.incomes = incomes; + } + + public List getExpenses() { + return expenses; + } + + public void setExpenses( List expenses ) { + this.expenses = expenses; + } + + public String getJsonIncomes() { + return serializeList( incomes ); + } + + public String getJsonExpenses() { + return serializeList( expenses ); + } + + public String getTemplate() { + return template; + } + + public void setTemplate( String template ) { + this.template = template; + } + + public String getJsonRates() { + if( rates == null ) { + CurrencyRateService rateService = ServiceContextUtils.getService( CurrencyRateService.class ); + //rates = rateService.loadLatestCurrencyRates(); + rates = rateService.loadAllCurrencyRates(); + } + return serializeList( rates ); + } + + public void setCustomChargeDate( String date ) { + if( StringUtils.hasText( date ) ) { + try { + date += "-01"; + Date d = new SimpleDateFormat("yyyy-MM-dd").parse( date ); + getModel().setChargeDate( d ); + } catch( ParseException e ) { + logger.warn( "提交的记账月份格式错误,忽略这个值。", e ); + } + } else { + getModel().setChargeDate( null ); + } + } + + public int getNature() { + return nature; + } + + public void setNature( int nature ) { + this.nature = nature; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeEditAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeEditAction.java new file mode 100644 index 0000000..0910287 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeEditAction.java @@ -0,0 +1,75 @@ +package com.ksa.web.struts2.action.finance.charge; + +import java.util.Date; +import java.util.List; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.Account; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.security.User; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.service.finance.AccountService; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.logistics.BookingNoteService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.util.StringUtils; +import com.opensymphony.xwork2.ModelDriven; + + +public class ChargeEditAction extends ChargeAction implements ModelDriven { + + private static final long serialVersionUID = -2776151163922960571L; + + @Override + protected String doExecute() throws Exception { + BookingNoteService service = ServiceContextUtils.getService( BookingNoteService.class ); + bookingNote = service.loadBookingNoteById( bookingNote.getId() ); + if( StringUtils.hasText( template ) ) { + // 以模板的方式添加 + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + List charges = chargeService.loadBookingNoteCharges( template, incomes, expenses ); + User currentUser = SecurityUtils.getCurrentUser(); + for( Charge charge : charges ) { + charge.setId( "" ); + charge.setCreatedDate( new Date() ); + charge.setCreator( currentUser ); + } + + } else /* if( ! BookingNoteState.isNone( bookingNote.getState() ) ) */{ + // 如果 BookingNote 已经存在状态(说明已经录入了费用) 则读取费用列表 + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + chargeService.loadBookingNoteCharges( bookingNote.getId(), incomes, expenses ); + } + // 这里利用 bookingNote.creator 属性显示当前用户信息。 + bookingNote.setCreator( SecurityUtils.getCurrentUser() ); + + if( bookingNote.getChargeDate() == null ) { + bookingNote.setChargeDate( new Date() ); + } + return SUCCESS; + } + + @Override + public String getJsonRates() { + if( rates == null ) { + Account account = null; + if( incomes != null && !incomes.isEmpty() ) { + for( Charge income : incomes ) { + if( income.getAccountState() >= 0 ) { + account = income.getAccount(); + } + } + } + // 按结算单的汇率计算 + if( account != null ) { + rates = ServiceContextUtils.getService( AccountService.class ).loadAccountCurrencyRates( account ); + } else { + CurrencyRateService rateService = ServiceContextUtils.getService( CurrencyRateService.class ); + rates = rateService.loadLatestCurrencyRates(bookingNote.getChargeDate()); + } + } + return serializeList( rates ); + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeQueryAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeQueryAction.java new file mode 100644 index 0000000..72eafa0 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeQueryAction.java @@ -0,0 +1,179 @@ +package com.ksa.web.struts2.action.finance.charge; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.ibatis.session.SqlSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.dao.AbstractQueryClause; +import com.ksa.dao.DateQueryClause; +import com.ksa.dao.QueryClause; +import com.ksa.dao.TextQueryClause; +import com.ksa.dao.mybatis.session.RowBounds; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.finance.query.ChargeStateQueryClause; +import com.ksa.web.struts2.action.finance.query.FinanceDirectionQueryClause; +import com.ksa.web.struts2.action.logistics.bookingnote.BookingNoteQueryAction; +import com.opensymphony.xwork2.ActionContext; + +public class ChargeQueryAction extends BookingNoteQueryAction { + + private static final long serialVersionUID = -2820784442841536220L; + private static final Logger log = LoggerFactory.getLogger( ChargeQueryAction.class ); + + protected Integer nature; + protected Integer direction; + protected Boolean settle; // 是否开单 + + protected static final Map chargeQueryClauses = new HashMap( 32 ); + + static { + for( String key : preparedQueryClauses.keySet() ) { + chargeQueryClauses.put( key, preparedQueryClauses.get( key ) ); + } + + chargeQueryClauses.put( "CHARGE_STATE", new ChargeStateQueryClause() ); + + chargeQueryClauses.put( "TARGET_ID", new TextQueryClause( "c.TARGET_ID" ) ); + chargeQueryClauses.put( "CHARGE_TYPE", new TextQueryClause( "c.TYPE" ) ); + chargeQueryClauses.put( "CURRENCY", new TextQueryClause( "c.CURRENCY_ID" ) ); + chargeQueryClauses.put( "DIRECTION", new FinanceDirectionQueryClause( "c.DIRECTION" ) ); + chargeQueryClauses.put( "NATURE", new FinanceDirectionQueryClause( "c.NATURE" ) ); + chargeQueryClauses.put( "INPUTOR", new TextQueryClause( "c.CREATOR_ID" ) ); + chargeQueryClauses.put( "INPUT_DATE", new DateQueryClause( "c.CREATED_DATE" ) ); + chargeQueryClauses.put( "ACCOUNT_CODE", new TextQueryClause( "a.code" ) ); + chargeQueryClauses.put( "SETTLE", new AbstractQueryClause( "c.ACCOUNT_ID" ) { + protected String getCompareCondition( String value, int index ) { + try { + StringBuilder sb = new StringBuilder( 64 ); + sb.append( " ( (" ) + .append( getColumnName() ) + .append( Boolean.parseBoolean( value ) ? " IS NOT NULL ) AND ( " : " IS NULL ) OR ( " ) + .append( getColumnName() ) + .append( Boolean.parseBoolean( value ) ? " <> '' " : " = '' " ) + .append( " ) ) " ); + return sb.toString(); + } catch( Exception e ) { } + return null; + } + }); + } + + @Override + public String execute() throws Exception { + SqlSession sqlSession = ServiceContextUtils.getService( SqlSession.class ); + if( sqlSession != null ) { + Map paras = getParameters(); + if( StringUtils.hasText( this.sort ) ) { + paras.put( "_sort", this.sort ); + paras.put( "_order", this.order ); + } + + try { + List gridDataList = sqlSession.selectList( getQueryDataStatement(), paras, new RowBounds( this.page, 1000 ) ); + if( gridDataList != null && !gridDataList.isEmpty() ) { + Set idSet = new HashSet(); // 不重复添加 BookingNote + TreeSet noteSet = new TreeSet(); + + for( Charge charge : gridDataList ) { + if( ! idSet.contains( charge.get_parentId() ) ) { + idSet.add( charge.get_parentId() ); + noteSet.add( charge.getBookingNote() ); + } + } + + List result = new LinkedList( gridDataList ); + for( BookingNote note : noteSet ) { + result.add( 0, note ); + } + + // gridDataArray = gridDataList.toArray(); + // gridDataCount = ( Integer ) sqlSession.selectOne( getQueryCountStatement(), paras ); + gridDataArray = result.toArray(); + gridDataCount = ( Integer ) sqlSession.selectOne( getQueryCountStatement(), paras ) + noteSet.size(); + } + } catch( Throwable e ) { + log.warn( "查询数据失败。", e ); + } + } + return SUCCESS; + } + + @Override + protected Map getParameters() { + ActionContext context = ActionContext.getContext(); + Map paras = context.getParameters(); + if( direction != null && ! paras.containsKey( "direction" ) ) { + paras.put( "DIRECTION", new String[] { direction.toString() } ); + + // 收入结算单需要费用经过审核 + if(direction == 1) { + paras.put( "CHARGE_STATE", new String[] { "8" } ); + } + } + if( nature != null && ! paras.containsKey( "nature" ) ) { + paras.put( "NATURE", new String[] { nature.toString() } ); + } + if( settle != null && ! paras.containsKey( "settle" ) ) { + paras.put( "SETTLE", new String[] { settle.booleanValue() ? "true" : "false" } ); + } + + if( ! StringUtils.hasText( this.sort ) ) { + this.sort = "bn.serial_number"; + this.order = "desc"; + } + + return super.getParameters(); + } + + @Override + protected Map getQueryClauseMap() { + return chargeQueryClauses; + } + + @Override + protected String getQueryCountStatement() { + return "count-finance-charge-query"; + } + + @Override + protected String getQueryDataStatement() { + return "grid-finance-charge-query"; + } + + public Integer getNature() { + return nature; + } + + public void setNature( Integer nature ) { + this.nature = nature; + } + + public Integer getDirection() { + return direction; + } + + public void setDirection( Integer direction ) { + this.direction = direction; + } + + public Boolean isSettle() { + return settle; + } + + public void setSettle( Boolean settle ) { + this.settle = settle; + } + + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeSaveAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeSaveAction.java new file mode 100644 index 0000000..d56631e --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeSaveAction.java @@ -0,0 +1,27 @@ +package com.ksa.web.struts2.action.finance.charge; + +import java.util.Date; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.logistics.BookingNoteService; +import com.ksa.service.security.util.SecurityUtils; + +public class ChargeSaveAction extends ChargeEditAction { + + private static final long serialVersionUID = -3776802985971150958L; + + @Override + protected String doExecute() throws Exception { + Date chargeDate = bookingNote.getChargeDate(); + BookingNoteService service = ServiceContextUtils.getService( BookingNoteService.class ); + bookingNote = service.loadBookingNoteById( bookingNote.getId() ); + bookingNote.setChargeDate( chargeDate ); + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + chargeService.saveBookingNoteCharges( bookingNote, incomes, expenses ); + + // 这里利用 bookingNote.creator 属性显示当前用户信息。 + bookingNote.setCreator( SecurityUtils.getCurrentUser() ); + return SUCCESS; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeStateAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeStateAction.java new file mode 100644 index 0000000..5217b0d --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ChargeStateAction.java @@ -0,0 +1,52 @@ +package com.ksa.web.struts2.action.finance.charge; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.BookingNoteChargeState; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + +/** + * 修改业务费用状态的操作。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class ChargeStateAction extends ChargeEditAction implements JsonAction { + + private static final long serialVersionUID = -3776802985971150958L; + + private JsonResult result; + + @Override + public String execute() throws Exception { + int state = bookingNote.getState(); + if( BookingNoteChargeState.isChecked( state ) ) { + if( ! SecurityUtils.isPermitted( "finance:charge-check" ) ) { + result = new JsonResult( ERROR, "对不起,您没有权限进行费用审核操作!" ); + return ERROR; + } + } + return super.execute(); + } + + @Override + public String doExecute() throws Exception { + ChargeService service = ServiceContextUtils.getService( ChargeService.class ); + bookingNote = service.updateBookingNoteChargeState( bookingNote, nature ); + String message = String.format( "成功更新业务 '%s' 的费用状态。", bookingNote.getCode() ); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message ); + return SUCCESS; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next() ); + } + return this.result; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ForeignChargeQueryAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ForeignChargeQueryAction.java new file mode 100644 index 0000000..e3c6b30 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/ForeignChargeQueryAction.java @@ -0,0 +1,19 @@ +package com.ksa.web.struts2.action.finance.charge; + +import com.ksa.web.struts2.action.logistics.bookingnote.BookingNoteQueryAction; + +public class ForeignChargeQueryAction extends BookingNoteQueryAction { + + private static final long serialVersionUID = -5728962722156241776L; + + @Override + protected String getQueryCountStatement() { + return "count-finance-profit-query"; + } + + @Override + protected String getQueryDataStatement() { + return "grid-finance-profit-query-foreign"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/NativeChargeQueryAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/NativeChargeQueryAction.java new file mode 100644 index 0000000..a8cd00e --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/NativeChargeQueryAction.java @@ -0,0 +1,19 @@ +package com.ksa.web.struts2.action.finance.charge; + +import com.ksa.web.struts2.action.logistics.bookingnote.BookingNoteQueryAction; + +public class NativeChargeQueryAction extends BookingNoteQueryAction { + + private static final long serialVersionUID = -7443894503539555697L; + + @Override + protected String getQueryCountStatement() { + return "count-finance-profit-query"; + } + + @Override + protected String getQueryDataStatement() { + return "grid-finance-profit-query-native"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleAction.java new file mode 100644 index 0000000..a789a53 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleAction.java @@ -0,0 +1,147 @@ +package com.ksa.web.struts2.action.finance.charge.single; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.struts2.json.JSONException; +import org.apache.struts2.json.JSONUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.bd.CurrencyRate; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.service.bd.CurrencyRateService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + + +public class ChargeSingleAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = -1007892464889907411L; + + private static Logger logger = LoggerFactory.getLogger( ChargeSingleAction.class ); + private static final Collection excludeProperties = new ArrayList( 8 ); + static { + excludeProperties.add( Pattern.compile( ".*account.*" ) ); + excludeProperties.add( Pattern.compile( ".*bookingNote.*" ) ); + excludeProperties.add( Pattern.compile( ".*partner.*" ) ); + excludeProperties.add( Pattern.compile( ".*month.*" ) ); + } + + protected String template; + protected BookingNote bookingNote = new BookingNote(); + protected List charges = new ArrayList(); + protected List rates; + protected int direction = 1; + protected int nature = 1; + + @Override + public String execute() throws Exception { + if( ! SecurityUtils.isPermitted( "finance:charge" ) ) { + return NO_PERMISSION; + } + return super.execute(); + } + + /*** + * 将费用列表序列化为 JSON 字符串。 + */ + protected String serializeList( List list ) { + try { + return JSONUtil.serialize( list, excludeProperties, null, false, true ); + } catch( JSONException e ) { + logger.warn( "序列化数据列表发生异常!", e ); + return "[]"; + } + } + + /** 获取记账日期 */ + protected Date computeChargeDate() { + // 以第一笔资金的记录时间为记账日期 + if( charges.size() <= 0 ) { + return null; + } + + Date chargeDate = new Date(); + for( Charge charge : charges ) { + if( charge.getCreatedDate().before( chargeDate ) ) { + chargeDate = charge.getCreatedDate(); + } + } + return chargeDate; + } + + @Override + public BookingNote getModel() { + return bookingNote; + } + + public List getCharges() { + return charges; + } + + public void setCharges( List incomes ) { + this.charges = incomes; + } + + public String getJsonCharges() { + return serializeList( charges ); + } + + public String getTemplate() { + return template; + } + + public void setTemplate( String template ) { + this.template = template; + } + + public String getJsonRates() { + if( rates == null ) { + CurrencyRateService rateService = ServiceContextUtils.getService( CurrencyRateService.class ); + rates = rateService.loadLatestCurrencyRates(); + } + return serializeList( rates ); + } + + public void setCustomChargeDate( String date ) { + if( StringUtils.hasText( date ) ) { + try { + date += "-01"; + Date d = new SimpleDateFormat("yyyy-MM-dd").parse( date ); + getModel().setChargeDate( d ); + } catch( ParseException e ) { + logger.warn( "提交的记账月份格式错误,忽略这个值。", e ); + } + } else { + getModel().setChargeDate( null ); + } + } + + public int getDirection() { + return direction; + } + + public void setDirection( int direction ) { + this.direction = direction; + } + + public int getNature() { + return nature; + } + + public void setNature( int nature ) { + this.nature = nature; + } + + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleEditAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleEditAction.java new file mode 100644 index 0000000..5caa72c --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleEditAction.java @@ -0,0 +1,50 @@ +package com.ksa.web.struts2.action.finance.charge.single; + +import java.util.Date; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.Charge; +import com.ksa.model.logistics.BookingNote; +import com.ksa.model.logistics.BookingNoteState; +import com.ksa.model.security.User; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.logistics.BookingNoteService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.util.StringUtils; +import com.opensymphony.xwork2.ModelDriven; + + +public class ChargeSingleEditAction extends ChargeSingleAction implements ModelDriven { + + private static final long serialVersionUID = -2776151163922960571L; + + @Override + protected String doExecute() throws Exception { + BookingNoteService service = ServiceContextUtils.getService( BookingNoteService.class ); + bookingNote = service.loadBookingNoteById( bookingNote.getId() ); + + if( StringUtils.hasText( template ) ) { + // 以模板的方式添加 + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + this.charges = chargeService.loadBookingNoteCharges( template, direction, nature ); + User currentUser = SecurityUtils.getCurrentUser(); + for( Charge charge : charges ) { + charge.setId( "" ); + charge.setCreatedDate( new Date() ); + charge.setCreator( currentUser ); + } + + } else if( ! BookingNoteState.isNone( bookingNote.getState() ) ) { + // 如果 BookingNote 已经存在状态(说明已经录入了费用) 则读取费用列表 + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + this.charges = chargeService.loadBookingNoteCharges( bookingNote.getId(), direction, nature ); + } + bookingNote.setCreator( SecurityUtils.getCurrentUser() ); + + if( bookingNote.getChargeDate() == null ) { + bookingNote.setChargeDate( new Date() ); + } + return SUCCESS; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleQueryAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleQueryAction.java new file mode 100644 index 0000000..6c52858 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleQueryAction.java @@ -0,0 +1,93 @@ +package com.ksa.web.struts2.action.finance.charge.single; + +import java.util.ArrayList; +import java.util.List; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.BookingNoteCharge; +import com.ksa.model.finance.Charge; +import com.ksa.model.finance.FinanceModel; +import com.ksa.service.finance.ChargeService; +import com.ksa.web.struts2.action.logistics.bookingnote.BookingNoteQueryAction; + +public class ChargeSingleQueryAction extends BookingNoteQueryAction { + + private static final long serialVersionUID = 7210295997960367238L; + + protected int nature; + protected int direction; + // true:费用列表为空时也显示;false:费用列表为空时不显示 + protected boolean showEmpty = true; + + @Override + public String execute() throws Exception { + String result = super.execute(); + ChargeService service = ServiceContextUtils.getService( ChargeService.class ); + if( showEmpty ) { // 不论有没费用 均显示 + for( Object obj : gridDataArray ) { + BookingNoteCharge bn = ( BookingNoteCharge ) obj; + bn.setCharges( service.loadBookingNoteCharges( bn.getId(), direction, nature ) ); + } + } else { + List bnList = new ArrayList( gridDataArray.length ); + for( Object obj : gridDataArray ) { + BookingNoteCharge bn = ( BookingNoteCharge ) obj; + List charges = service.loadBookingNoteCharges( bn.getId(), direction, nature ); + if( charges != null && charges.size() > 0 ) { + bn.setCharges( charges ); + bnList.add( bn ); + } + } + gridDataArray = bnList.toArray(); + } + return result; + } + + @Override + protected String getQueryCountStatement() { + return "count-finance-charge-fetch"; + } + + @Override + protected String getQueryDataStatement() { + return "grid-finance-charge-fetch"; + } + + + public int getNature() { + return nature; + } + + public void setNature( int nature ) { + if( nature >= 0 ) { + this.nature = FinanceModel.NATIVE; + } else { + this.nature = FinanceModel.FOREIGN; + } + } + + public int getDirection() { + return direction; + } + + public void setDirection( int direction ) { + if( direction >= 0 ) { + this.direction = FinanceModel.INCOME; + } else { + this.direction = FinanceModel.EXPENSE; + } + } + + public boolean isShowEmpty() { + return showEmpty; + } + + public boolean getShowEmpty() { + return showEmpty; + } + + public void setShowEmpty( boolean showEmpty ) { + this.showEmpty = showEmpty; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleSaveAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleSaveAction.java new file mode 100644 index 0000000..3c67922 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleSaveAction.java @@ -0,0 +1,24 @@ +package com.ksa.web.struts2.action.finance.charge.single; + +import java.util.Date; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.logistics.BookingNoteService; + +public class ChargeSingleSaveAction extends ChargeSingleEditAction { + + private static final long serialVersionUID = -3776802985971150958L; + + @Override + protected String doExecute() throws Exception { + Date chargeDate = bookingNote.getChargeDate(); + BookingNoteService service = ServiceContextUtils.getService( BookingNoteService.class ); + bookingNote = service.loadBookingNoteById( bookingNote.getId() ); + bookingNote.setChargeDate( chargeDate ); + + ChargeService chargeService = ServiceContextUtils.getService( ChargeService.class ); + charges = chargeService.saveBookingNoteCharges( bookingNote, charges, direction, nature ); + return SUCCESS; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleStateAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleStateAction.java new file mode 100644 index 0000000..be79215 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/charge/single/ChargeSingleStateAction.java @@ -0,0 +1,53 @@ +package com.ksa.web.struts2.action.finance.charge.single; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.BookingNoteChargeState; +import com.ksa.service.finance.ChargeService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + +/** + * 修改业务费用状态的操作。 + * + * @author 麻文强 + * + * @since v0.0.1 + */ +public class ChargeSingleStateAction extends ChargeSingleEditAction implements JsonAction { + + private static final long serialVersionUID = -3776802985971150958L; + + private JsonResult result; + + @Override + public String execute() throws Exception { + int state = bookingNote.getState(); + if( BookingNoteChargeState.isChecked( state ) ) { + if( ! SecurityUtils.isPermitted( "finance:charge-check" ) ) { + result = new JsonResult( ERROR, "对不起,您没有权限进行费用审核操作!" ); + return ERROR; + } + } + return super.execute(); + } + + @Override + public String doExecute() throws Exception { + ChargeService service = ServiceContextUtils.getService( ChargeService.class ); + + bookingNote = service.updateBookingNoteChargeState( bookingNote, direction, nature ); + String message = String.format( "成功更新业务 '%s' 的费用状态。", bookingNote.getCode() ); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message ); + return SUCCESS; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next() ); + } + return this.result; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/component/FinanceSelectionAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/component/FinanceSelectionAction.java new file mode 100644 index 0000000..215b5ff --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/component/FinanceSelectionAction.java @@ -0,0 +1,40 @@ +package com.ksa.web.struts2.action.finance.component; + +import com.ksa.web.struts2.action.DefaultActionSupport; + + +public class FinanceSelectionAction extends DefaultActionSupport { + + private static final long serialVersionUID = -8520764711769003125L; + + protected Integer nature; + protected Integer direction; + protected Boolean settle; // 是否开单 + + public Integer getNature() { + return nature; + } + + public void setNature( Integer nature ) { + this.nature = nature; + } + + public Integer getDirection() { + return direction; + } + + public void setDirection( Integer direction ) { + this.direction = direction; + } + + public Boolean getSettle() { + return settle; + } + + public void setSettle( Boolean settle ) { + this.settle = settle; + } + + + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAction.java new file mode 100644 index 0000000..d43a58c --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAction.java @@ -0,0 +1,42 @@ +package com.ksa.web.struts2.action.finance.invoice; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.model.finance.Invoice; +import com.ksa.service.finance.AccountService; +import com.ksa.service.security.util.SecurityUtils; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.DefaultActionSupport; +import com.opensymphony.xwork2.ModelDriven; + + +public class InvoiceAction extends DefaultActionSupport implements ModelDriven { + + private static final long serialVersionUID = -8482885003379839294L; + + protected Invoice invoice = new Invoice(); + + @Override + public String execute() throws Exception { + if( ! SecurityUtils.isPermitted( "finance:invoice" ) ) { + return NO_PERMISSION; + } + return super.execute(); + } + + @Override + protected String doExecute() throws Exception { + invoice.setCreator( SecurityUtils.getCurrentUser() ); + String accountId = invoice.getAccount().getId(); + if( StringUtils.hasText( accountId ) ) { + AccountService service = ServiceContextUtils.getService( AccountService.class ); + invoice.setAccount( service.loadAccountById( accountId ) ); + } + return SUCCESS; + } + + @Override + public Invoice getModel() { + return invoice; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAssignAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAssignAction.java new file mode 100644 index 0000000..3cadeca --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceAssignAction.java @@ -0,0 +1,40 @@ +package com.ksa.web.struts2.action.finance.invoice; + +import org.springframework.util.StringUtils; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.InvoiceService; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + + +public class InvoiceAssignAction extends InvoiceAction implements JsonAction { + + private static final long serialVersionUID = 35148775615646492L; + + private JsonResult result; + + @Override + public String doExecute() throws Exception { + InvoiceService service = ServiceContextUtils.getService( InvoiceService.class ); + invoice = service.assignInvoiceToAccount( invoice, invoice.getAccount() ); + String message = ""; + if( invoice.getAccount() == null || ! StringUtils.hasText( invoice.getAccount().getId() ) ) { + message = String.format( "销账还原成功。发票号:'%s'。", invoice.getCode() ); + } else { + message = String.format( "销账成功。发票号:'%s',结算/对账单号:'%s'。", invoice.getCode(), invoice.getAccount().getCode() ); + } + addActionMessage( message ); + result = new JsonResult( SUCCESS, message, invoice ); + return SUCCESS; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next(), invoice ); + } + return this.result; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceDeleteAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceDeleteAction.java new file mode 100644 index 0000000..e4a08ab --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceDeleteAction.java @@ -0,0 +1,33 @@ +package com.ksa.web.struts2.action.finance.invoice; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.InvoiceService; +import com.ksa.web.struts2.action.JsonAction; +import com.ksa.web.struts2.action.model.JsonResult; + + +public class InvoiceDeleteAction extends InvoiceAction implements JsonAction { + + private static final long serialVersionUID = -52336492842713670L; + + private JsonResult result; + + @Override + public String doExecute() throws Exception { + InvoiceService service = ServiceContextUtils.getService( InvoiceService.class ); + invoice = service.removeInvoice( invoice ); + String message = String.format( "成功删除发票:'%s'。", invoice.getCode() ); + addActionMessage( message ); + result = new JsonResult( SUCCESS, message, invoice ); + return SUCCESS; + } + + @Override + public Object getJsonResult() { + if( this.hasActionErrors() ) { + this.result = new JsonResult( ERROR, this.getActionErrors().iterator().next(), invoice ); + } + return this.result; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceEditAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceEditAction.java new file mode 100644 index 0000000..facdcc7 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceEditAction.java @@ -0,0 +1,20 @@ +package com.ksa.web.struts2.action.finance.invoice; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.InvoiceService; + + +public class InvoiceEditAction extends InvoiceAction { + + /** + * + */ + private static final long serialVersionUID = 3380407353975122628L; + + @Override + protected String doExecute() throws Exception { + InvoiceService service = ServiceContextUtils.getService( InvoiceService.class ); + invoice = service.loadInvoiceById( invoice.getId() ); + return SUCCESS; + } +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceQueryAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceQueryAction.java new file mode 100644 index 0000000..dc8a684 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceQueryAction.java @@ -0,0 +1,137 @@ +package com.ksa.web.struts2.action.finance.invoice; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.ibatis.session.SqlSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.dao.AbstractQueryClause; +import com.ksa.dao.DateQueryClause; +import com.ksa.dao.QueryClause; +import com.ksa.dao.TextQueryClause; +import com.ksa.dao.mybatis.session.RowBounds; +import com.ksa.util.StringUtils; +import com.ksa.web.struts2.action.data.GridDataActionSupport; +import com.ksa.web.struts2.action.finance.query.FinanceDirectionQueryClause; +import com.ksa.web.struts2.action.finance.query.InvoiceStateQueryClause; +import com.opensymphony.xwork2.ActionContext; + + +public class InvoiceQueryAction extends GridDataActionSupport { + + private static final long serialVersionUID = 5486180868391240224L; + + private static final Logger log = LoggerFactory.getLogger( InvoiceQueryAction.class ); + + protected static final Map preparedQueryClauses = new HashMap( 32 ); + // 初始化 queryClause + static { + preparedQueryClauses.put( "INVOICE_STATE", new InvoiceStateQueryClause() ); + preparedQueryClauses.put( "CODE", new TextQueryClause( "i.CODE" ) ); + preparedQueryClauses.put( "TYPE", new TextQueryClause( "i.TYPE" ) ); + preparedQueryClauses.put( "TARGET_ID", new TextQueryClause( "i.TARGET_ID" ) ); + preparedQueryClauses.put( "DIRECTION", new FinanceDirectionQueryClause( "i.DIRECTION" ) ); + preparedQueryClauses.put( "CURRENCY", new TextQueryClause( "i.CURRENCY_ID" ) ); + preparedQueryClauses.put( "CREATED_DATE", new DateQueryClause( "i.CREATED_DATE" ) ); + preparedQueryClauses.put( "INPUTOR", new TextQueryClause( "i.CREATOR_ID" ) ); + preparedQueryClauses.put( "ACCOUNT_CODE", new TextQueryClause( "a.CODE" ) ); + preparedQueryClauses.put( "ACCOUNT_ID", new TextQueryClause( "i.ACCOUNT_ID" ) ); + preparedQueryClauses.put( "SETTLE", new AbstractQueryClause( "i.ACCOUNT_ID" ) { + protected String getCompareCondition( String value, int index ) { + try { + StringBuilder sb = new StringBuilder( 64 ); + sb.append( " ( (" ) + .append( getColumnName() ) + .append( Boolean.parseBoolean( value ) ? " IS NOT NULL ) AND ( " : " IS NULL ) OR ( " ) + .append( getColumnName() ) + .append( Boolean.parseBoolean( value ) ? " <> '' " : " = '' " ) + .append( " ) ) " ); + return sb.toString(); + } catch( Exception e ) { } + return null; + } + }); + } + + protected Object[] gridDataArray = new Object[0]; + protected int gridDataCount = 0; + + private static final String QUERY_DATA_STATEMENT = "grid-finance-invoice-query"; + private static final String QUERY_COUNT_STATEMENT= "count-finance-invoice-query"; + + @Override + public String execute() throws Exception { + SqlSession sqlSession = ServiceContextUtils.getService( SqlSession.class ); + if( sqlSession != null ) { + Map paras = getParameters(); + if( StringUtils.hasText( this.sort ) ) { + paras.put( "_sort", this.sort ); + paras.put( "_order", this.order ); + } + + try { + List gridDataList = sqlSession.selectList( getQueryDataStatement(), paras, new RowBounds( this.page, this.rows ) ); + if( gridDataList != null && !gridDataList.isEmpty() ) { + gridDataArray = gridDataList.toArray(); + Integer queryCount = sqlSession.selectOne( getQueryCountStatement(), paras ); + gridDataCount = queryCount.intValue(); + } + } catch( Throwable e ) { + log.warn( "查询数据失败。", e ); + } + } + return SUCCESS; + } + + protected String getQueryDataStatement() { + return QUERY_DATA_STATEMENT; + } + + protected String getQueryCountStatement() { + return QUERY_COUNT_STATEMENT; + } + + protected Map getParameters() { + ActionContext context = ActionContext.getContext(); + Map paras = context.getParameters(); + Collection clauseList = new ArrayList( 16 ); + + Set keys = paras.keySet(); + for( String key : keys ) { + if( preparedQueryClauses.containsKey( key.toUpperCase() ) ) { + QueryClause qc = preparedQueryClauses.get( key.toUpperCase() ); + Object value = paras.get( key ); + if( value.getClass().isArray() ) { + Collection clauses = qc.compute( ( String[] ) value ); + if( clauses != null && clauses.size() > 0 ) { + clauseList.addAll( clauses ); + } + } + } + } + if( clauseList.size() > 0 ) { + paras.put( "queryClauses", clauseList.toArray() ); + } + + return paras; + } + + + @Override + protected Object[] queryGridData() { + return gridDataArray; + } + + @Override + protected int queryGridDataCount() { + return gridDataCount; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceSaveAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceSaveAction.java new file mode 100644 index 0000000..dd47a2b --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/invoice/InvoiceSaveAction.java @@ -0,0 +1,19 @@ +package com.ksa.web.struts2.action.finance.invoice; + +import com.ksa.context.ServiceContextUtils; +import com.ksa.service.finance.InvoiceService; + + +public class InvoiceSaveAction extends InvoiceAction { + + private static final long serialVersionUID = 3806493572595278852L; + + @Override + protected String doExecute() throws Exception { + InvoiceService service = ServiceContextUtils.getService( InvoiceService.class ); + invoice = service.saveInvoice( invoice ); + addActionMessage( String.format( "发票号为 '%s' 的发票信息保存成功。", invoice.getCode() ) ); + return SUCCESS; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/profit/ProfitQueryAction.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/profit/ProfitQueryAction.java new file mode 100644 index 0000000..82fa47d --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/profit/ProfitQueryAction.java @@ -0,0 +1,20 @@ +package com.ksa.web.struts2.action.finance.profit; + +import com.ksa.web.struts2.action.logistics.bookingnote.BookingNoteQueryAction; + + +public class ProfitQueryAction extends BookingNoteQueryAction { + + private static final long serialVersionUID = -7568580059497504302L; + + @Override + protected String getQueryCountStatement() { + return "count-finance-profit-query"; + } + + @Override + protected String getQueryDataStatement() { + return "grid-finance-profit-query"; + } + +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/AccountStateQueryClause.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/AccountStateQueryClause.java new file mode 100644 index 0000000..a5c2e0b --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/AccountStateQueryClause.java @@ -0,0 +1,60 @@ +package com.ksa.web.struts2.action.finance.query; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.dao.AbstractQueryClause; +import com.ksa.dao.QueryClause; + + +public class AccountStateQueryClause extends AbstractQueryClause implements QueryClause { + + private static final Logger logger = LoggerFactory.getLogger( AccountStateQueryClause.class ); + + public AccountStateQueryClause() { + super( "a.STATE" ); + } + + @Override + protected String getCompareCondition( String value, int index ) { + try { + int v = Integer.parseInt( value ); + StringBuilder sb = new StringBuilder( 64 ); + if( v == 0 ) { // 新建的结算单 + sb.append( " ( a.STATE = 0 ) " ); + } else if( v > 0 ) { + sb.append( " ( " ) + .append( getColumnName() ) + .append( " & " ) + .append( v ) + .append( " > 0 ) " ); + } + return sb.toString(); + } catch( Exception e ) { + logger.warn( "获取查询比较片段时发生异常,忽略此比较。", e ); + } + return null; + } + + @Override + public Collection compute(String[] values) { + Collection clauses = super.compute( values ); + if( clauses != null && clauses.size() > 0 ) { + StringBuilder sb = new StringBuilder( 16 * clauses.size() ); + sb.append( " ( " ); + int i = 0; + for( String clause : clauses ) { + if( i++ > 0 ) { + sb.append( " OR " ); + } + sb.append( clause ); + } + sb.append( " ) " ); + clauses.clear(); + clauses.add( sb.toString() ); + } + return clauses; + }; +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/ChargeStateQueryClause.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/ChargeStateQueryClause.java new file mode 100644 index 0000000..b766e9f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/ChargeStateQueryClause.java @@ -0,0 +1,64 @@ +package com.ksa.web.struts2.action.finance.query; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.dao.AbstractQueryClause; +import com.ksa.dao.QueryClause; + + +public class ChargeStateQueryClause extends AbstractQueryClause implements QueryClause { + + private static final Logger logger = LoggerFactory.getLogger( ChargeStateQueryClause.class ); + + public ChargeStateQueryClause() { + super( "bn.STATE" ); + } + + @Override + protected String getCompareCondition( String value, int index ) { + try { + int v = Integer.parseInt( value ); + StringBuilder sb = new StringBuilder( 64 ); + if( v > 0 ) { + sb.append( " ( " ) + .append( getColumnName() ) + .append( " & " ) + .append( v ) + .append( " > 0 ) " ); + } else if( v == 0 ) { // 已开单 + sb.append( " ( c.ACCOUNT_ID IS NOT NULL AND c.ACCOUNT_ID <> '' ) " ); + } else if( v == -1 ) { + sb.append( " ( c.ACCOUNT_ID IS NULL OR c.ACCOUNT_ID = '' ) " ); + } else { + return null; + } + return sb.toString(); + } catch( Exception e ) { + logger.warn( "获取查询比较片段时发生异常,忽略此比较。", e ); + } + return null; + } + + @Override + public Collection compute(String[] values) { + Collection clauses = super.compute( values ); + if( clauses != null && clauses.size() > 0 ) { + StringBuilder sb = new StringBuilder( 16 * clauses.size() ); + sb.append( " ( " ); + int i = 0; + for( String clause : clauses ) { + if( i++ > 0 ) { + sb.append( " OR " ); + } + sb.append( clause ); + } + sb.append( " ) " ); + clauses.clear(); + clauses.add( sb.toString() ); + } + return clauses; + }; +} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/FinanceDirectionQueryClause.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/FinanceDirectionQueryClause.java new file mode 100644 index 0000000..745e639 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/FinanceDirectionQueryClause.java @@ -0,0 +1,56 @@ +package com.ksa.web.struts2.action.finance.query; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.dao.AbstractQueryClause; +import com.ksa.dao.QueryClause; + + +public class FinanceDirectionQueryClause extends AbstractQueryClause implements QueryClause { + + private static final Logger logger = LoggerFactory.getLogger( FinanceDirectionQueryClause.class ); + + public FinanceDirectionQueryClause( String columnName) { + super( columnName ); + } + + @Override + protected String getCompareCondition( String value, int index ) { + try { + int v = Integer.parseInt( value ); + StringBuilder sb = new StringBuilder( 64 ); + sb.append( " ( " ) + .append( getColumnName() ) + .append( " = " ) + .append( v ) + .append( " ) " ); + return sb.toString(); + } catch( Exception e ) { + logger.warn( "获取查询比较片段时发生异常,忽略此比较。", e ); + } + return null; + } + + @Override + public Collection compute(String[] values) { + Collection clauses = super.compute( values ); + if( clauses != null && clauses.size() > 0 ) { + StringBuilder sb = new StringBuilder( 16 * clauses.size() ); + sb.append( " ( " ); + int i = 0; + for( String clause : clauses ) { + if( i++ > 0 ) { + sb.append( " OR " ); + } + sb.append( clause ); + } + sb.append( " ) " ); + clauses.clear(); + clauses.add( sb.toString() ); + } + return clauses; + }; +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/InvoiceStateQueryClause.java b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/InvoiceStateQueryClause.java new file mode 100644 index 0000000..c15a1d8 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/java/com/ksa/web/struts2/action/finance/query/InvoiceStateQueryClause.java @@ -0,0 +1,62 @@ +package com.ksa.web.struts2.action.finance.query; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ksa.dao.AbstractQueryClause; +import com.ksa.dao.QueryClause; + + +public class InvoiceStateQueryClause extends AbstractQueryClause implements QueryClause { + + private static final Logger logger = LoggerFactory.getLogger( InvoiceStateQueryClause.class ); + + public InvoiceStateQueryClause() { + super( "a.STATE" ); + } + + @Override + protected String getCompareCondition( String value, int index ) { + try { + int v = Integer.parseInt( value ); + StringBuilder sb = new StringBuilder( 64 ); + if( v == 0 ) { // 未销账 + sb.append( " ( i.ACCOUNT_ID IS NULL OR i.ACCOUNT_ID = '' ) " ); + } else { + sb.append( " ( i.ACCOUNT_ID IS NOT NULL AND i.ACCOUNT_ID <> '' AND ( a.STATE & 32 " ); + // TODO 修改字面常量 + if( v == 32 ) { + sb.append( " > 0 ) ) " ); + } else { + sb.append( " = 0 ) ) " ); + } + } + return sb.toString(); + } catch( Exception e ) { + logger.warn( "获取查询比较片段时发生异常,忽略此比较。", e ); + } + return null; + } + + @Override + public Collection compute(String[] values) { + Collection clauses = super.compute( values ); + if( clauses != null && clauses.size() > 0 ) { + StringBuilder sb = new StringBuilder( 16 * clauses.size() ); + sb.append( " ( " ); + int i = 0; + for( String clause : clauses ) { + if( i++ > 0 ) { + sb.append( " OR " ); + } + sb.append( clause ); + } + sb.append( " ) " ); + clauses.clear(); + clauses.add( sb.toString() ); + } + return clauses; + }; +} \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/js/finance/utils.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/js/finance/utils.js new file mode 100644 index 0000000..d81e6fd --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/js/finance/utils.js @@ -0,0 +1,54 @@ +(function($){ + /* KSA - finance utils 初始化命名空间 */ + ksa.finance = ksa.finance || {}; + $.extend( ksa.finance, { + /** + * 弹出选择费用窗口 + */ + selectCharges : function( callback, paras, title ) { + paras = paras || {}; + top.$.open( { + width : 1000, + height : 600, + src : ksa.buildUrl( "/component/finance", "charge-selection", paras ), + title : title || "选择费用" + }, callback ); + }, + /** + * 弹出选择费用窗口 + */ + selectInvoices : function( callback, paras, title ) { + paras = paras || {}; + top.$.open( { + width : 1000, + height : 600, + src : ksa.buildUrl( "/component/finance", "invoice-selection", paras ), + title : title || "选择发票" + }, callback ); + }, + /** + * 弹出选择托单窗口 + */ + selectBookingNotes : function( callback, paras, title ) { + paras = paras || {}; + top.$.open( { + width : 1000, + height : 600, + src : ksa.buildUrl( "/component/finance", "bookingnote-selection", paras ), + title : title || "选择托单" + }, callback ); + }, + /** + * 弹出选择托单窗口 + */ + selectChargeTemplates : function( callback, paras, title ) { + paras = paras || {}; + top.$.open( { + width : 1000, + height : 600, + src : ksa.buildUrl( "/component/finance", "charge-template-selection", paras ), + title : title || "选择费用模板" + }, callback ); + } + } ); +})(jQuery); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.ftl new file mode 100644 index 0000000..4371e58 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.ftl @@ -0,0 +1,38 @@ + + + +账单管理 + + + + +
+ + + + <@shiro.hasPermission name="finance:account-check"> + + + + <@shiro.hasPermission name="finance:account-settle"> + + + + +
+
+
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.js new file mode 100644 index 0000000..8110298 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/account/default.js @@ -0,0 +1,145 @@ +$(function(){ + var $winWidth = 1000; + var $winHeight = 600; + + $("#go_processing, #go_checking, #go_checked, #go_unchecked, #go_settled").hide(); + + var $grid = $('#data_grid').datagrid({ + url: ksa.buildUrl( "/data/finance/account", "query" ), + fit : true, + border:false, + pageSize: 5, + fitColumns: false, + columns:[ GET_ACCOUNT_TABLE_COLUMN( { "a.type": true } ) ], + onDblClickRow:function() {$("#btn_edit").click();}, + onClickRow:function(){ + $("#go_processing, #go_checking, #go_checked, #go_unchecked, #go_settled").hide(); + var row = $grid.datagrid( "getSelected" ); + var state = parseState( row.state ); + if( state == STATE_NONE ) { $("#go_processing").show(); } + else if( state == STATE_PROCESSING ) { $("#go_checking").show(); } + else if( state == STATE_CHECKING ) { $("#go_checked, #go_unchecked").show(); } + else if( state == STATE_CHECKED ) { $("#go_settled").show(); } + }, + rowStyler : function( i, row ) { + var check = checkDeadline( row ); + if( check < 0 ) { + // 已超期 + return "background:#F00"; + } else if( check == 0 ) { + // 接近deadline + return "background:#FC3"; + } + return ""; + }, + onLoadSuccess : function( data ) { + var over = 0; // 过期数量 + var warning = 0; // 警示数量 + if( data.rows != null && data.rows.length > 0 ) { + $.each( data.rows, function(i, row){ + var check = checkDeadline( row ); + if( check < 0 ) { + over++; + } else if( check == 0 ) { + warning++; + } + } ); + } + if( over > 0 ) { + top.$.messager.error( "有超过付款截止日还未付款的账单!", "账单过期" ); + } else if( warning > 0 ) { + top.$.messager.info( "有账单接近付款截止日还未付款,请注意。", "注意" ); + } + } + }); + /*** + * 检查账单是否已经超期:1. 未超期,0. 将要超期,-1:已超期 + */ + function checkDeadline( row, delta ) { + if( !row.paymentDate && row.deadline ) { + delta = delta || 5; + var deadline = ksa.utils.parseDate( row.deadline ); + var now = new Date(); + if( now >= deadline ) { + return -1; + } else if( now.setDate( now.getDate() + delta ) >= deadline ){ + return 0; + } + } + return 1; + } + + // 编辑事件 + $("#btn_edit").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行编辑/查看操作。"); + return; + } + // 打开编辑页面 + top.$.open({ + width:$winWidth, + height:$winHeight, + + modal : false, + collapsible : true, + title: ( row.direction == 1? "结算单":"对账单") + "【" + row.code + "】", + src : ksa.buildUrl( "/dialog/finance/account", ( row.state == 0 ? "create" : "edit" ), { id : row.id } ) + }, function(){ + $grid.datagrid( "reload" ); + }); + }); + + $("#go_processing").click(function(){ changeState(1, "确定生成账单吗?
账单生成后将不能修改【汇率】信息。"); }); + $("#go_checking").click(function(){ changeState(2, "确定将账单提交审核吗?"); }); + $("#go_checked").click(function(){ changeState(8, "确定账单通过审核吗?"); }); + $("#go_unchecked").click(function(){ changeState(1, "确定将账单打回进行修正吗?"); }); + $("#go_settled").click(function(){ changeState(32, "确定账单结算完毕吗?"); }); + + function changeState( newState, message ) { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行相应操作。"); + return; + } + $.messager.confirm( message, function( ok ){ + if( ok ) { + $.ajax({ + url: ksa.buildUrl( "/dialog/finance/account", "state" ), + data: { id : row.id, state : newState }, + success: function( result ) { + try { + if (result.status == "success") { + $grid.datagrid( "reload" ); + } + else { parent.$.messager.error( result.message ); } + } catch (e) { } + } + }); + } + } ); + return false; + } + + var STATE_NONE = "新建"; + var STATE_PROCESSING = "开票中" ; + var STATE_CHECKING = "审核中"; + var STATE_CHECKED = "结算中"; + var STATE_SETTLED = "结算完毕"; + + // 解析托单的状态 返回可读的状态值 + function parseState( state ) { + if( state & 0x20 ) { + return STATE_SETTLED; + } else if( state & 0x8 ) { + return STATE_CHECKED; + } else if( state & 0x2 ) { + return STATE_CHECKING; + } else if( state & 0x1 ) { + return STATE_PROCESSING; + } else { + return STATE_NONE; + } + }; + +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.ftl new file mode 100644 index 0000000..d6169f3 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.ftl @@ -0,0 +1,23 @@ + + + +业务费用列表 +<#assign securityUtilsClass = "@com.ksa.service.security.util.SecurityUtils" /> +<#assign currentUser = stack.findValue("${securityUtilsClass}@getCurrentUser()") /> + + + + +
+
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.js new file mode 100644 index 0000000..13f8560 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/portal/finance/charge/default.js @@ -0,0 +1,68 @@ +$(function(){ + var $winWidth = 1000; + var $winHeight = 600; + + var $grid = $('#data_grid').datagrid({ + url: ksa.buildUrl( "/data/finance/profit", "query" ), + queryParams : { 'CREATOR_ID' : CURRENT_USER_ID }, + fit : true, + border:false, + pageSize: 5, + fitColumns: false, + columns:[ GET_PROFIT_TABLE_COLUMN( { 'cargo_container':true, 'profit_gather':false, 'income_gather':false, 'expense_gather':false } ) ], + onDblClickRow:function() { edit(); }, + rowStyler:function(index,row,css){ + if ( row.state == -1 ){ + return 'text-decoration: line-through;'; + } + }, // jira KSA-17 + toolbar:[ { + text:'编辑/查看...', + cls: 'btn-warning', + float: 'right', + iconCls:'icon-edit icon-white', + handler:function(){ edit(); } + } ] + }); + + // 编辑事件 + function edit() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + top.$.messager.warning("请选择一条数据后,再进行删除操作。"); + return; + } + // 打开编辑页面 + top.$.open({ + width:$winWidth, + height:$winHeight, + modal : false, + collapsible : true, + + title:"费用信息:" + row.code, + src : ksa.buildUrl( "/dialog/finance/charge", "view", { id : row.id } ) + }, function(){ + $grid.datagrid( "reload" ); + }); + } + + var STATE_CHECKED = "审核通过"; + var STATE_CHECKING = "审核中"; + var STATE_ENTERING = "录入中"; + var STATE_NONE = "暂未录入"; + var STATE_DELETED = "业务作废"; + // 解析托单的状态 返回可读的状态值 + function parseState( state ) { + if( state == -1 ) { + return STATE_DELETED; + } else if( state & 0x8 ) { + return STATE_CHECKED; + } else if( state & 0x2 ) { + return STATE_CHECKING; + } else if( state & 0x1 ) { + return STATE_ENTERING; + } else { + return STATE_NONE; + } + }; +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts-plugin.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts-plugin.xml new file mode 100644 index 0000000..d0e606d --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts-plugin.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-account.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-account.xml new file mode 100644 index 0000000..aa1fa08 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-account.xml @@ -0,0 +1,92 @@ + + + + + + /ui/finance/account/default.ftl + + + /ui/finance/account/default-doinvoice.ftl + + + + + + + /portal/finance/account/default.ftl + + + + + + + + + application/xhtml+xml + attachment;filename="${filename}" + + + + + /ui/finance/account/edit.ftl + + + + /ui/finance/account/create.ftl + + + /ui/finance/account/account.ftl + + + /ui/finance/account/account-excel.ftl + + + /ui/finance/account/invoice.ftl + + + + /ui/finance/account/excel/account${direction}.ftl + + + + /ui/finance/account/edit.ftl + /ui/finance/account/account.ftl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + application/json + true + false + true + gridData + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge-single.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge-single.xml new file mode 100644 index 0000000..8eb3fe7 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge-single.xml @@ -0,0 +1,47 @@ + + + + + + /ui/finance/charge-single/default.ftl + + + /ui/finance/charge-single/checking.ftl + + + + + + + + + /ui/finance/charge-single/view.ftl + + + /ui/finance/charge-single/view.ftl + + + + + + + + + + + + application/json + true + false + true + gridData + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge.xml new file mode 100644 index 0000000..78de01b --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-charge.xml @@ -0,0 +1,49 @@ + + + + + + /ui/finance/charge/default.ftl + + + /ui/finance/charge/checking.ftl + + + + + + /portal/finance/charge/default.ftl + + + + + + + /ui/finance/charge/view.ftl + /ui/finance/charge/view.ftl + + + /ui/finance/charge/view.ftl + /ui/finance/charge/view.ftl + + + + + + + + + + + + application/json + true + false + true + gridData + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-component.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-component.xml new file mode 100644 index 0000000..1d0a124 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-component.xml @@ -0,0 +1,22 @@ + + + + + + + /ui/finance/component/charge-selection.ftl + + + /ui/finance/component/invoice-selection.ftl + + + /ui/finance/component/bookingnote-selection.ftl + + + /ui/finance/component/charge-template-selection.ftl + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-invoice.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-invoice.xml new file mode 100644 index 0000000..586c0c3 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-invoice.xml @@ -0,0 +1,45 @@ + + + + + + /ui/finance/invoice/default.ftl + + + + + + + /ui/finance/invoice/view.ftl + + + /ui/finance/invoice/view.ftl + + + /ui/finance/invoice/view.ftl + + + + + + + + + + + + + + + + application/json + true + false + true + gridData + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-profit.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-profit.xml new file mode 100644 index 0000000..d779b87 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-profit.xml @@ -0,0 +1,20 @@ + + + + + + + application/json + true + false + true + gridData + + + + + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-recordbill.xml b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-recordbill.xml new file mode 100644 index 0000000..991a1ed --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/struts2/struts-finance-recordbill.xml @@ -0,0 +1,45 @@ + + + + + + + + application/xhtml+xml + attachment;filename="${filename}" + + + + + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-im.ftl + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-im.ftl + /ui/finance/business/recordbill-ly.ftl + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-ex.ftl + /ui/finance/business/recordbill-im.ftl + + + + + + + + + application/xhtml+xml + attachment;filename="${filename}" + + + + + /ui/finance/business/debitNote.ftl + + + diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-detail.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-detail.ftl new file mode 100644 index 0000000..de38a17 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-detail.ftl @@ -0,0 +1,109 @@ +<#-- 判断状态的静态类 --> +<#assign state = model.state?c /> +<#assign stateClass = "@com.ksa.model.finance.AccountState" /> + + + + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+

<#-- 第一行 --> +
+ +
+ value="${model.createdDate?date}" /> +
+
+
+ +
+ value="${model.deadline?date}" /> +
+
+
+ +
+ value="${model.paymentDate?date}" /> +
+

<#-- 第二行 --> +
+ +
+ +
+
+
<#-- end 业务基本信息 --> + <#--
+ +
+
end 利润统计 --> +
<#-- end north --> +
+
+ +
+
+
+
+
+
<#-- end center --> +
+
+ + + +<#if stack.findValue("${stateClass}@isSettled(${state})")> + 状态:结算完毕 +<#elseif stack.findValue("${stateClass}@isChecked(${state})")> + 状态:<#if model.direction == 1>收款中<#else>付款中 + <@shiro.hasPermission name="finance:account-settle"> + + + +<#elseif stack.findValue("${stateClass}@isChecking(${state})")> + 状态:开票中 + <@shiro.hasPermission name="finance:account-check"> + + + +<#elseif stack.findValue("${stateClass}@isProcessing(${state})")> + 状态:审核中 + <@shiro.hasPermission name="finance:account-check"> + + + +<#elseif stack.findValue("${stateClass}@isNone(${state})")> + <#if model.id??> + 状态:待审核 + + <#else> + 状态:新建 + + + +<#if ! stack.findValue("${stateClass}@isSettled(${state})")> + + + +
\ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.ftl new file mode 100644 index 0000000..6042952 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.ftl @@ -0,0 +1,70 @@ +<#-- Excel 导出形式的结算单页面 --> + + + +<#setting number_format="0.##"> <#-- 格式化数字输出 --> +<#if model.direction == 1> + <#assign directionName = "结算单" /> + <#assign chargeStyle = "font-weight:bold;color:#BD362F;" /> +<#else> + <#assign directionName = "对账单" /> + <#assign chargeStyle = "font-weight:bold;color:#51A351;" /> + +${directionName} 【 ${model.code!} 】 + + + + +
+ +
+
+ +
+
+
+
+
<#-- end center --> +
+
+ +
提示:右键点击业务信息表的表头可以过滤需要导出的内容。
+ + +
+
<#-- end div --> + + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.js new file mode 100644 index 0000000..8bfacec --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-excel.js @@ -0,0 +1,91 @@ +$(function(){ + + // 业务托单列表 + var $bnGrid = $("#bn_datagrid").datagrid({ + loadEmptyMsg:'注意 暂时没有任何相关的业务信息', + pagination: false, + remoteSort:false, + fit: true, + fitColumns:false, + columns:[ getBookingNoteTableColumn() ], + onDblClickRow : function(){ viewNote(); } + }).datagrid('loadData', BOOKING_NOTES || [] ); + + function getBookingNoteTableColumn() { + var NOT_SHOW_COLUMNS = [ "creator_name", "agent_name", "customs_broker_name", "customs_code", "customs_date" ]; + var columns = []; + $.each( GET_BOOKINGNOTE_TABLE_COLUMN({ + type:false, + created_date:false,customer_name:false,invoice_number:true,creator_name:false,agent_name:false,cargo_name:false,customs_code:false, + //volumn:false,weight:false,quantity:false, + cargo_container:true, // 空运不显示 + route_name:true, + departure_port:true, + departure_date:true, + destination_port:true, + destination_date:true + }), function(i,d){ + if( $.inArray( d.field, NOT_SHOW_COLUMNS ) < 0 ) { + d.sortable=false; + columns.push( d ); + } + }); + return columns; + }; + + // 数据列表 + var $chargeGrid = $('#charge_datagrid').datagrid({ + fit : true, + pagination : false, + fitColumns : false, + rownumbers : false, + columns : CHARGE_COLUMNS, + rowStyler:function(i){ + if ( i == CHARGE_DATA.length - 1 ){ // TOTAL 行 + if( DIRECTION == 1 ) { + return 'font-weight:bold;color:#BD362F;'; + } else { + return 'font-weight:bold;color:#51A351;'; + } + } + }, + onDblClickRow : function(){ alert("todo view charge"); } + }).datagrid( "loadData", CHARGE_DATA ); + + $("#btn_download").click(function() { + var form = $( "
" ).appendTo( $("body") ); + form.form("submit", { + url: ksa.buildUrl( "/dialog/finance/account", "account-download"), + onSubmit: function() { + var option = $("#bn_datagrid").datagrid("options"); + var tr = option.finder.getTr( $("#bn_datagrid")[0], 0, "body", 2 ); + $("td:visible",tr).each( function(i,td) { + $("").val( $(td).attr("field") ).appendTo( form ); + }); + $("").val( $("#account_id").val() ).appendTo( form ); + } + }); + return false; + }); + + function viewNote(){ + var row = $bnGrid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行查看业务操作。"); + return; + } + // 打开编辑页面 + top.$.open({ + width:1000, + height:600, + modal : false, + collapsible : true, + + title:"编辑托单信息:" + row.code, + src : ksa.buildUrl( "/dialog/logistics", "edit-" + row.type.toLowerCase(), { id : row.id, code : row.code } ) + }, function(){ + window.location.href=window.location.href; + }); + } + +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-query-condition.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-query-condition.js new file mode 100644 index 0000000..7d19966 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-query-condition.js @@ -0,0 +1,82 @@ +var ACCOUNT_QUERY_CONDITION = [ + { title:"状态", name:"ACCOUNT_STATE", init: function( target, condition ) { + $(target).append(""); + var input = $("").width( $(target).width() - 50 ); + input.append(""); + input.append(""); + input.append(""); + input.append(""); + input.append(""); + input.append(""); + input.append(""); + input.append(""); + input.append(""); + input.append(""); + $(target).append(input); + input.combobox( {multiple:true,editable:false} ); + } }, + { title:"接单日期", name:"CREATED_DATE", type:"date" }, + { title:"客户", name:"CUSTOMER_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-all" )} }, + +// { title:"发票号", name:"INVOCE_NUMBER", type:"text" }, +// { title:"操作员", name:"CREATOR_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "security-user-all" ),codeField : "id" } }, +// { title:"销售担当", name:"SALER_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "security-user-all" ),codeField : "id" } }, +// { title:"承运人", name:"CARRIER_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-bytype", { typeId : '20-department-cyr' } ) } }, +// { title:"船代", name:"SHIPPING_AGENT_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-bytype", { typeId : '20-department-cd' } ) } }, +// { title:"代理", name:"AGENT_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-bytype", { typeId : '20-department-dls' } ) } }, + + { title:"品名", name:"CARGO_NAME", type:"textarea" }, +// { title:"箱类箱型", name:"CARGO_CONTAINER", type:"text" }, +// { title:"唛头", name:"SHIPPING_MARK", type:"textarea" }, + + { title:"提单号", name:"BL_NO", type:"text" }, + { title:"发货人", name:"SHIPPER_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-all" )} }, + { title:"通知人", name:"CONSIGNEE_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-all" )} }, + + // TODO combo 如何传入多个参数 +// { title:"出发地", name:"DEPARTURE", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '30-state' } ), valueField : "name"} }, +// { title:"目的地", name:"DESTINATION", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '31-port-sea' } ), valueField : "name"} }, + + { title:"起运港", name:"DEPARTURE_PORT", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '31-port-sea' } ), valueField : "name"} }, + { title:"目的港", name:"DESTINATION_PORT", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '31-port-sea' } ), valueField : "name"} }, + { title:"装货港", name:"LOADING_PORT", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '31-port-sea' } ), valueField : "name"} }, + { title:"卸货港", name:"DISCHARGE_PORT", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '31-port-sea' } ), valueField : "name"} }, + + { title:"离港日", name:"DEPARTURE_DATE", type:"date" }, + { title:"到港日", name:"DESTINATION_DATE", type:"date" }, + { title:"送货日", name:"DELIVER_DATE", type:"date" } //, + + +// { title:"航线", name:"ROUTE", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '33-route-sea' } ), valueField : "name"} }, +// { title:"船名/航班", name:"ROUTE_NAME", type:"text" }, +// { title:"航次", name:"ROUTE_CODE", type:"text" }, + +// { title:"报关行", name:"CUSTOMS_BROKER_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-bytype", { typeId : '20-department-bgh' } ) } }, +// { title:"报关单号", name:"CUSTOMS_CODE", type:"text" }, +// { title:"报关日期", name:"CUSTOMS_DATE", type:"date" }, +// { title:"退单号", name:"CUSTOMS_CODE", type:"text" }, +// { title:"退单日期", name:"RETURN_DATE", type:"date" }, + +// { title:"车队", name:"VEHICLE_TEAM_ID", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-partner-bytype", { typeId : '20-department-chedui' } ) } }, +// { title:"车型", name:"VEHICLE_TYPE", type:"combo", option:{url:ksa.buildUrl( "/data/combo", "bd-data-bytype", { typeId : '08-vehicle' } ), valueField : "name"} }, +// { title:"退单号", name:"VEHICLE_NUMBER", type:"text" } +]; \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-table-column.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-table-column.js new file mode 100644 index 0000000..891d10a --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account-table-column.js @@ -0,0 +1,118 @@ +/** true: 表示显示相应名称的列,false: 表示隐藏相应名称的列。*/ +var ACCOUNT_TABLE_SHOW_COLUMN = { + 'a.state' : true, + 'a.type' : false, + 'a.code' : true, + 'a.target_id' : true, + 'a.charges' : true, // 组合而成的费用明细 + 'a.charge_gather' : true, + 'a.created_date' : true, + 'a.invoice_gather' : true, + 'a.deadline' : true, + 'a.payment_date' : true, + 'a.creator_name' : false, + 'a.note' : false +}; +function GET_ACCOUNT_TABLE_COLUMN( showColumn ) { + // 解析 费用明细 + function parseChargesDetail( charges, direction ) { + if( !charges || charges.length <= 0 ) return ""; + var title = charges[0].type + ":" + charges[0].amount + charges[0].currency.name; + var detail = ""; + for( var i = 1; i < charges.length; i++ ) { + ( i % 2 != 0 ) ? detail += "," : detail += "
"; + title += "\n"; + var c = charges[i]; + detail += ""; + title += c.type + ":" + c.amount + c.currency.name; + } + return "
" + detail +"
" ; + }; + + // 汇总费用/票据的金额 + function gatherDetail( details ) { + if( !details || details.length <= 0 ) return ""; + var map = {}; + $.each( details, function(i,v){ + if( map[ v.currency.name ] ) { + map[ v.currency.name ] += v.amount; + } else { + map[ v.currency.name ] = v.amount; + } + } ); + var detail = ""; + for( var k in map ) { + detail += "
" + k + ":" + parseFloat( map[k] ).toFixed( 1 ) + "
"; + } + return detail; + } + + var STATE_NONE = "录入中"; + var STATE_PROCESSING = "审核中"; + var STATE_CHECKING= "开票中"; + var STATE_CHECKED = function( direction ){ return direction == 1 ? "收款中" : "付款中"; }; + var STATE_SETTLED = "结算完毕"; + + // 解析托单的状态 返回可读的状态值 + function parseState( data ) { + var state = data.state; + if( state & 0x20 ) { + return STATE_SETTLED; + } else if( state & 0x8 ) { + return STATE_CHECKED( data.direction ); + } else if( state & 0x2 ) { + return STATE_CHECKING; + } else if( state & 0x1 ) { + return STATE_PROCESSING; + } else { + return STATE_NONE; + } + }; + + function parseType( direction, nature ) { + var type = ( direction == 1 ? "结算单" : "对账单" ); + if( nature == -1 ) { + type += "(境外)"; + } + return type; + }; + + showColumn = $.extend( {}, ACCOUNT_TABLE_SHOW_COLUMN, showColumn || {} ); + return [ + { field:'a.state', title:'状态', width:50, align:'center', hidden:!showColumn["a.state"], + formatter: function(v, data){ return parseState( data ); }, + styler : function(v,row) { + var state = parseState( row ); + var css = "font-weight:bold;"; + if( state == STATE_CHECKED( row.direction ) ) { return css + "color:#51A351"; } + else if( state == STATE_CHECKING ) { return css + "color:#FAA732"; } + else if( state == STATE_PROCESSING ) { return css + "color:#04C"; } + else if( state == STATE_NONE ) { return css + "color:#BD362F"; } + } }, + { field:'a.type', title:'账单类型', width:70, align:'center', sortable:false, hidden:!showColumn["a.type"], + formatter: function(v, data){ return parseType( data.direction, data.nature ); }, + styler : function(v,row) { + return row.direction == 1 ? "color:#BD362F" : "color:#51A351"; + } }, + { field:'a.code', title:'编号', width:130, sortable:true, hidden:!showColumn["a.code"], + formatter: function(v, data){ return data.code; } }, + { field:'a.target_id', title:'结算对象', width:100, sortable:true, hidden:!showColumn["a.target_id"], + formatter: function(v, data){ return data.target.name; } }, + { field:'a.charges', title:'费用明细', width:250, hidden:!showColumn["a.charges"], + formatter: function(v, data){ return parseChargesDetail( data.charges, data.direction ); } }, + { field:'a.charge_gather', title:'费用汇总', width:100, hidden:!showColumn["a.charge_gather"], + formatter: function(v, data){ return "
" + gatherDetail( data.charges ) + "
"; } }, + { field:'a.invoice_gather', title:'开票汇总', width:100, hidden:!showColumn["a.invoice_gather"], + formatter: function(v, data){ return "
" + gatherDetail( data.invoices ) + "
"; } }, + { field:'a.created_date', title:'开单日期', width:75, sortable:true, align:"center", hidden:!showColumn["a.created_date"], + formatter : function(v, data) { return ksa.utils.dateFormatter( data.createdDate ); } }, + { field:'a.deadline', title:'付款截止日', width:75, sortable:true, align:"center", hidden:!showColumn["a.deadline"], + formatter : function(v, data) { return ksa.utils.dateFormatter( data.deadline ); } }, + { field:'a.payment_date', title:'结清日期', width:75, sortable:true, align:"center", hidden:!showColumn['a.payment_date'], + formatter : function(v, data) { return ksa.utils.dateFormatter( data.paymentDate ); } }, + { field:'a.creator_name', title:'创建人', width:60, sortable:true, align:"center", hidden:!showColumn['a.creator_name'], + formatter : function(v, data) { return data.creator.name; } }, + { field:'a.note', title:'备注', width:150, sortable:true, hidden:!showColumn['a.note'], + formatter : function(v, data) { return data.note; } } + ]; +}; \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.css b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.css new file mode 100644 index 0000000..0d6538f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.css @@ -0,0 +1,24 @@ +form, div#dialog_container { padding: 5px; } + +/* 结算单明细部分 */ +.control-group { margin-bottom: 0;margin-bottom: 5px\9; } + +.form-inline .control-label { width:75px; } +.form-inline .controls { margin-left: 75px; } + +input, textarea { width: 150px; } +.input-small { width:75px; } +.input-xxlarge { width: 634px; } + +.add-on span input.combo-text { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +/* 费用列表部分 */ +div.charge-container { float:left; width:75%; height:100%; margin:0; } +div.currency-container { float:left; width:25%; height:100%; margin:0; } + +/** 工具栏 */ +div.bottom-bar span.title {margin:3px 20px 0 0;} diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.ftl new file mode 100644 index 0000000..75cd44f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.ftl @@ -0,0 +1,30 @@ + + + +<#if model.direction == 1> + <#assign directionName = "结算单" /> +<#else> +<#assign directionName = "对账单" /> + +${directionName} 【 ${model.code!} 】 + + + + +
"> + <#include "account-detail.ftl" /> +
<#-- end form --> + + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.js new file mode 100644 index 0000000..80c367f --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/account.js @@ -0,0 +1,307 @@ +function validateCustomer() { return ( $("#target").combobox("getValue") != $("#target").combobox("getText") ); }; +$(function(){ + // 初始化布局 + $("#tab_container")._outerHeight( $(window).height() - 75 ); + $("#account_container").layout(); + + // 结算对象选择框 + $("#target").combobox({ + url : ksa.buildUrl( "/data/combo", "bd-partner-without-lock" ), + onSelect : function( record ){ + $rateGrid.datagrid( "load", { id: $("#account_id").val(), "target.id": record.id } ); + // 自动更新编码 + //$("#code").val( "J" + ksa.utils.dateFormatter( new Date() ) + record.code + "1" ); + //现在采用服务器获取的方式 + $.ajax({ + url: ksa.buildUrl( "/dialog/finance/account", "compute-code", { "code" : record.code } ), + success: function( result ) { + try { + if (result.status == "success") { + $("#code").val( result.message ); // 展示从服务器获取的编码 + } + else { parent.$.messager.error( result.message ); } + } catch (e) { } + } + }); + + var d = ksa.utils.parseDate( $("#createdDate").val() ); + var pp = record.pp; + d.setDate( d.getDate() + pp ); + $("#deadline").datebox( "setValue", (d.getFullYear() + "-" + ( d.getMonth() + 1 ) + "-" + d.getDate()) ); + } + }); + + // 费用列表 + var $chargeGrid = $("#charge_datagrid").datagrid({ + loadEmptyMsg:'注意 暂时没有加入任何费用信息', + pagination: false, + remoteSort:false, + fit: true, + fitColumns:false, + columns:[ + $.merge( [ { field:'c.id', title:'标识', width:50, hidden:true, toggleable : false, + formatter: function(v, data){ return data.id + ""; } } ], + GET_CHARGE_TABLE_COLUMN( { 'a.code': false, 'bn.mawb': false, 'bn.customer_name' : false } ) ) ], + onDblClickRow : function(){ viewCharge(); }, + toolbar : ( STATE > 0 ) ? null : [{ + text:'添加...', + cls:'btn-primary', + iconCls:'icon-plus icon-white', + handler:function(){ add(); } + }, '-', { + text:'删除', + cls:'btn-danger', + iconCls:'icon-trash icon-white', + handler:function(){ remove(); } + }, { + text:'撤销修改', + cls:'btn-info', + float:'right', + iconCls:'icon-share-alt icon-white', + handler:function(){ undo(); } + }, '|', { + text:'查看业务信息', + cls:'btn-success', + float:'right', + handler:function(){ viewNote(); } + }, { + text:'查看费用明细', + cls:'btn-success', + float:'right', + handler:function(){ viewCharge(); } + }] + }).datagrid('loadData', CHARGES || [] ); + + function add( target ) { + ksa.finance.selectCharges( function( records ) { + var oldData = $chargeGrid.datagrid( "getRows" ); + var map = generateDataMap( oldData ); + var newData = []; + var warning = false; + $.each( records, function(i,v){ + if( validateCharge( v.data ) ) { + if( !map[v.value] ) { // 不添加重复值 + newData.push( v.data ); + } + } else { + warning = true; // 有非法值 + } + }); + if( warning ) { + top.$.messager.warning("请为"+ACCOUNT_NAME+"添加合理的费用,"+(DIRECTION == 1 ? "支出" : "收入" )+"费用以及已经开单的费用将被忽略。"); + } + $chargeGrid.datagrid('loadData', $.merge( newData, oldData ) ); + }, { direction: DIRECTION, /*nature: NATURE,*/ settle: "false" }, "选择"+ACCOUNT_NAME+"的费用明细"); + // 国内/境外暂时都显示! + + // 验证费用 + function validateCharge( charge ) { + // 收支方向正确 并且 费用不属于别的结算单 + return charge.direction == DIRECTION && !charge.settle; + }; + function generateDataMap( list ) { + var map = {}; + $.each( list, function(i, d){ map[d.id] = d; }); + return map; + }; + }; + + function viewNote() { + var row = $chargeGrid.datagrid( "getSelected" ); + if( ! row ) { + parent.$.messager.warning("请选择一条数据后,再进行业务查看操作。"); + return; + } + var note = row.bookingNote; + // 打开编辑页面 + top.$.open({ + width:1000, + height:600, + modal : false, + collapsible : true, + + title:"托单信息:" + note.code, + src : ksa.buildUrl( "/dialog/logistics", "edit-" + note.type.toLowerCase(), { id : note.id, code : note.code } ) + }); + return; + }; + + function viewCharge() { + var row = $chargeGrid.datagrid( "getSelected" ); + if( ! row ) { + parent.$.messager.warning("请选择一条数据后,再进行费用查看操作。"); + return; + } + var note = row.bookingNote; + // 打开编辑页面 + top.$.open({ + width:1000, + height:600, + + modal : false, + collapsible : true, + title:"费用信息:" + note.code, + src : ksa.buildUrl( "/dialog/finance/charge", "view", { id : note.id, nature: 1 } ) + }); + return; + }; + + function remove() { + var row = $chargeGrid.datagrid( "getSelected" ); + if( ! row ) { + parent.$.messager.warning("请选择一条数据后,再进行删除操作。"); + return; + } + + parent.$.messager.confirm( "确定删除费用 '" + row.type + "' 吗?", function( ok ){ + if( ok ) { $chargeGrid.datagrid( "deleteRow", $chargeGrid.datagrid("getRowIndex",row) ); /*markDirty( target );*/ } + } ); + }; + /* 撤销费用修改 */ + function undo() { + /*if( ! isDirty( target ) ) { + parent.$.messager.warning("数据没有发生任何修改。"); + return; + }*/ + parent.$.messager.confirm( "确定撤销对费用清单的所有修改吗?", function( ok ){ + if( ok ) { $chargeGrid.datagrid("rejectChanges");/* markClean( target ); */} + } ); + }; + + // 汇率列表 + var $lastIndex = -1; + var $rateGrid = $('#currency_datagrid').datagrid({ + url : ksa.buildUrl( "/data/grid/currency", "account"), + queryParams : { id: $("#account_id").val(), "target.id": $("#target").val() }, + fit : true, + pagination : false, + fitColumns : false, + columns : [ [ + { field:'id', title:'标识', hidden:true, + formatter: function(v, data){ return ""; } }, + { field:'code', title:'货币代码', width:70, + formatter: function(v, data){ return data.currency.code; } }, + { field:'name', title:'货币名称', width:70, + formatter: function(v, data){ return data.currency.name; } }, + { field:'rate', title:'汇率', width:70, align:'right', + formatter: function(v, data){ return v + ""; }, + editor:{ type:'numberbox',options:{precision:3} }, + styler:function(){return 'color:blue;';} } + ] ], + rowStyler:function(index,row,css){ + if ( !row.id ){ + return 'color:red;font-weight:bold;'; + } + }, + onClickRow: ( STATE > 1 ) ? function(){} : function( rowIndex ) { + $rateGrid.datagrid('endEdit', $lastIndex); + if ( $lastIndex != rowIndex ) { + $rateGrid.datagrid('beginEdit', rowIndex); + $lastIndex = rowIndex; + } else { + $lastIndex = -1; + } + } + }); + + // 保存 + $("#dialog_save").click(function(){ + $rateGrid.datagrid('endEdit', $lastIndex); + var tables = [ $chargeGrid[0], $rateGrid[0] ]; + var names = [ "charges", "rates" ]; + for( var i = 0; i < names.length; i++ ) { + var table = tables[i]; + var option = $(table).datagrid("options"); + var rows = $(table).datagrid("getRows"); + for( var j = 0; j < rows.length; j++ ) { + var tr = option.finder.getTr( table, j, "body", 2 ); + $("input[type='hidden']", tr).each(function(){ + $(this).attr("name", names[i] + "["+j+"]." +$(this).attr("name") ); + }); + } + }; + }); + + // 关闭 + $("#dialog_close").bind("click", function(){ + top.$.close(); return false; + }); + + // ------------- 状态变更操作 + $("#go_processing").click(function(){ changeState(1, "确定提交审核"+ACCOUNT_NAME+"吗?
账单提交审核后将不能修改【汇率】信息。"); return false; }); + $("#go_checking").click(function(){ changeState(2, "确定"+ACCOUNT_NAME+"通过审核吗?"); return false; }); + $("#return_processing").click(function(){ changeState(0, "确定将"+ACCOUNT_NAME+"打回进行修正吗?"); return false; }); + $("#go_checked").click(function(){ changeState(8, "确定完成发票开具,进行"+(DIRECTION == 1 ? "收款" : "付款")+"确认吗?"); return false; }); + $("#return_checking").click(function(){ changeState(1, "确定对"+ACCOUNT_NAME+"重新进行审核吗?"); return false; }); + $("#return_checked").click(function(){ changeState(2, "确定重新开具发票吗?"); return false; }); + $("#go_settled").click(function(){ + var paymentDate = $("#paymentDate").val(); + if( !paymentDate || paymentDate == "" ) { + parent.$.messager.confirm( "结清日期还未输入,是否今天结清?", function( ok ){ + if( ok ) { + var d = new Date(); + paymentDate = (d.getFullYear() + "-" + ( d.getMonth() + 1 ) + "-" + d.getDate()); + $("#paymentDate").datebox( "setValue", paymentDate ); + changeState( 32, false, { "paymentDate":paymentDate } ); + } + } ); + } else { + changeState( 32, "确定"+ACCOUNT_NAME+"结算完毕吗?", { "paymentDate":paymentDate } ); + } + return false; + }); + + function changeState( newState, message, params ) { + function doChangeState() { + $.ajax({ + url: ksa.buildUrl( "/dialog/finance/account", "state" ), + data: $.extend( { id : $("#account_id").val(), state : newState }, params ), + success: function( result ) { + try { + if (result.status == "success") { + $("form.easyui-form").attr("action", ksa.buildUrl( "/dialog/finance/account", "account" ) ); + $("input[type='hidden']", $("div.grid-container") ).remove(); + $("form.easyui-form")[0].submit(); + } + else { parent.$.messager.error( result.message ); } + } catch (e) { } + } + }); + } + if( message ) { + parent.$.messager.confirm( message, function( ok ){ + if( ok ) { + doChangeState(); + } + } ); + } else { + doChangeState(); + } + return false; + } + + // 新增导出默认结算单的功能。 + $("#btn_download").click(function() { + var form = $( "
" ).appendTo( $("body") ); + form.form("submit", { + url: ksa.buildUrl( "/dialog/finance/account", "account-download"), + onSubmit: function() { + // 导出默认列 + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").val( $("#account_id").val() ).appendTo( form ); + } + }); + return false; + }); +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/create.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/create.ftl new file mode 100644 index 0000000..a2e24c9 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/create.ftl @@ -0,0 +1,34 @@ + + + +<#if model.direction == 1> + <#assign directionName = "结算单" /> +<#else> +<#assign directionName = "对账单" /> + +新建${directionName} + + + + +
+
+
"> + <#include "account-detail.ftl" /> +
<#-- end form --> +
<#-- end tab-panel 业务基本信息 --> +
<#-- end tab --> + + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default-doinvoice.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default-doinvoice.ftl new file mode 100644 index 0000000..6463ec7 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default-doinvoice.ftl @@ -0,0 +1,46 @@ + + + +账单管理 + + + + + + +
+

账单列表

+ + + + + <@shiro.hasPermission name="finance:account-check"> + + + + <@shiro.hasPermission name="finance:account-settle"> + + + + + +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.ftl new file mode 100644 index 0000000..cb35b52 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.ftl @@ -0,0 +1,63 @@ +<#if model.nature==-1> + <#assign accountName = "境外账单" /> +<#elseif model.direction == 1> + <#assign accountName = "结算单" /> +<#else> + <#assign accountName = "对账单" /> + + + + +${accountName!}管理 + + + + + + +
+

${accountName!}管理

+ + + + <@shiro.hasPermission name="finance:account-check"> + + + + + + <@shiro.hasPermission name="finance:account-settle"> + + + + + + + + +
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.js new file mode 100644 index 0000000..591b638 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/default.js @@ -0,0 +1,267 @@ +$(function(){ + var $winWidth = 1000; + var $winHeight = 600; + + $("#data_container").height( $(window).height() - 105 ); + $("#data_container").layout(); + + $("#go_processing, #go_checking, #go_checked, #go_unchecked, #go_settled").hide(); + + var $grid = $('#data_grid').datagrid({ + url: ksa.buildUrl( "/data/finance/account", "query", { direction : ( NATURE == -1 ? undefined : DIRECTION ), nature : NATURE } ), + queryParams : QUERY_PARAMS, + fit : true, + border:false, + pageSize: 20, + fitColumns: false, + columns:[ GET_ACCOUNT_TABLE_COLUMN() ], + onDblClickRow:function() { + if( $("#btn_edit").size() > 0 ) { + $("#btn_edit").click(); + } else { + $("#btn_doinvoice").click(); + } + }, + onClickRow:function(){ + $("#btn_delete,#go_processing, #go_checking, #return_processing, #go_checked, #go_settled,#return_checked, #return_checking").hide(); + var row = $grid.datagrid( "getSelected" ); + var state = parseState( row.state ); + if( state == STATE_NONE ) { $("#btn_delete, #go_processing").show(); } + else if( state == STATE_PROCESSING ) { $("#go_checking, #return_processing").show(); } + else if( state == STATE_CHECKING ) { $("#go_checked, #return_checking").show(); } + else if( state == STATE_CHECKED ) { $("#go_settled, #return_checked").show(); } + }, + rowStyler : function( i, row ) { + var check = checkDeadline( row ); + if( check < 0 ) { + // 已超期 + return "background:#F00"; + } else if( check == 0 ) { + // 接近deadline + return "background:#FC3"; + } + return ""; + }, + onLoadSuccess : function( data ) { + var over = 0; // 过期数量 + var warning = 0; // 警示数量 + if( data.rows != null && data.rows.length > 0 ) { + $.each( data.rows, function(i, row){ + var check = checkDeadline( row ); + if( check < 0 ) { + over++; + } else if( check == 0 ) { + warning++; + } + } ); + } + if( over > 0 ) { + $.messager.error( "有超过付款截止日还未付款的账单!", "账单过期" ); + } else if( warning > 0 ) { + $.messager.info( "有账单接近付款截止日还未付款,请注意。", "注意" ); + } + } + }); + /*** + * 检查账单是否已经超期:1. 未超期,0. 将要超期,-1:已超期 + */ + function checkDeadline( row, delta ) { + if( !row.paymentDate && row.deadline ) { + delta = delta || 5; + var deadline = ksa.utils.parseDate( row.deadline ); + var now = new Date(); + if( now >= deadline ) { + return -1; + } else if( now.setDate( now.getDate() + delta ) >= deadline ){ + return 0; + } + } + return 1; + } + + // 新建事件 + $("#btn_add").click( function() { + // 打开新建页面 + $.open({ + width:$winWidth, + height:$winHeight, + + modal : false, + collapsible : true, + title: "新建" + ACCOUNT_NAME, + src : ksa.buildUrl( "/dialog/finance/account", "create", {direction:DIRECTION, nature: NATURE} ) + }, function(){ + $grid.datagrid( "reload" ); + }); + }); + + // 编辑事件 + $("#btn_edit").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行编辑/查看操作。"); + return; + } + // 打开编辑页面 + $.open({ + width:$winWidth, + height:$winHeight, + + modal : false, + collapsible : true, + title: ACCOUNT_NAME + "【" + row.code + "】", + src : ksa.buildUrl( "/dialog/finance/account", "edit", { id : row.id } ) + }, function(){ + $grid.datagrid( "reload" ); + }); + }); + + // 编辑事件 - 直接进入开票界面 + $("#btn_doinvoice").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一个账单后,再进行开票操作。"); + return; + } + // 打开编辑页面 + $.open({ + width:$winWidth, + height:$winHeight, + + modal : false, + collapsible : true, + title: ACCOUNT_NAME + "【" + row.code + "】开票确认", + src : ksa.buildUrl( "/dialog/finance/account", ( row.state == 0 ? "create" : "edit" ), { id : row.id, selected: 2 } ) // 默认打开第2个tab页,也就是开票页 + }, function(){ + $grid.datagrid( "reload" ); + }); + }); + + // 删除事件 + $("#btn_delete").click( function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行删除操作。"); + return; + } + + $.messager.confirm( "确定删除" + ACCOUNT_NAME +" '" + row.code + "' 吗?", function( ok ){ + if( ok ) { + $.ajax({ + url: ksa.buildUrl( "/dialog/finance/account", "delete" ), + data: { id : row.id }, + success: function( result ) { + try { + if (result.status == "success") { + $.messager.success( result.message ); + $grid.datagrid( "reload" ); + } + else { $.messager.error( result.message ); } + } catch (e) { } + } + }); + } + } ); + }); + + $("#go_processing").click(function(){ changeState(1, "确定提交审核"+ACCOUNT_NAME+"吗?
账单提交审核后将不能修改【汇率】信息。"); return false; }); + $("#go_checking").click(function(){ changeState(2, "确定"+ACCOUNT_NAME+"通过审核吗?"); return false; }); + $("#return_processing").click(function(){ changeState(0, "确定将"+ACCOUNT_NAME+"打回进行修正吗?"); return false; }); + $("#go_checked").click(function(){ changeState(8, "确定完成发票开具,进行"+(DIRECTION == 1 ? "收款" : "付款")+"确认吗?"); return false; }); + $("#return_checking").click(function(){ changeState(1, "确定对"+ACCOUNT_NAME+"重新进行审核吗?"); return false; }); + $("#return_checked").click(function(){ changeState(2, "确定重新开具发票吗?"); return false; }); + $("#go_settled").click(function(){ changeState(32, "确定"+ACCOUNT_NAME+"结算完毕吗?"); }); + + function changeState( newState, message ) { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行相应操作。"); + return; + } + $.messager.confirm( message, function( ok ){ + if( ok ) { + $.ajax({ + url: ksa.buildUrl( "/dialog/finance/account", "state" ), + data: { id : row.id, state : newState }, + success: function( result ) { + try { + if (result.status == "success") { + $grid.datagrid( "reload" ); + } + else { parent.$.messager.error( result.message ); } + } catch (e) { } + } + }); + } + } ); + return false; + } + + + var STATE_NONE = "待审核"; + var STATE_PROCESSING = "审核中"; + var STATE_CHECKING= "开票中"; + var STATE_CHECKED = ( DIRECTION == 1 ? "收款中" : "付款中" ); + var STATE_SETTLED = "结算完毕"; + + // 解析托单的状态 返回可读的状态值 + function parseState( state ) { + if( state & 0x20 ) { + return STATE_SETTLED; + } else if( state & 0x8 ) { + return STATE_CHECKED; + } else if( state & 0x2 ) { + return STATE_CHECKING; + } else if( state & 0x1 ) { + return STATE_PROCESSING; + } else { + return STATE_NONE; + } + }; + + + // 绑定热键事件 + ksa.hotkey.bindButton( $(".tool-bar button.btn") ); + + // 初始化账单查询组件 + $("#query").compositequery({ + onClear:function() { + $grid.datagrid( "reload", {} ); + }, + onQuery:function( queryString ) { + $grid.datagrid( "load", queryString ); + }, + conditions : ACCOUNT_QUERY_CONDITION + }); + + // 新增导出默认结算单的功能。 + $("#btn_download").click(function() { + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + $.messager.warning("请选择一条数据后,再进行相应操作。"); + return; + } + + var form = $( "
" ).appendTo( $("body") ); + form.form("submit", { + url: ksa.buildUrl( "/dialog/finance/account", "account-download"), + onSubmit: function() { + // 导出默认列 + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").appendTo( form ); + $("").val( row.id ).appendTo( form ); + } + }); + return false; + }); +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/edit.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/edit.ftl new file mode 100644 index 0000000..1ea77ec --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/edit.ftl @@ -0,0 +1,27 @@ + + + +<#if model.direction == 1> + <#assign directionName = "结算单" /> +<#else> +<#assign directionName = "对账单" /> + +编辑${directionName} + + + +
+
'"> +
<#-- end tab-panel 业务基本信息 --> +
'"> +
<#-- end tab-panel 业务基本信息 --> +<@shiro.hasAnyPermissions name="finance:account-check,finance:account-settle"> +
'"> +
<#-- end tab-panel 业务基本信息 --> + +
<#-- end tab --> + + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account-1.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account-1.ftl new file mode 100644 index 0000000..a27b17c --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account-1.ftl @@ -0,0 +1,144 @@ + + + + + 14 + + + + + + + False + False + + + + + + + + + + + + 供应名称: + ${model.target.name!} + 账期: + ${accountBeginDate!} + + ${accountEndDate!} + + <#-- 空行 --> + + <#list bookingNoteHeader as bnHeader> + ${bnHeader} + + <#assign chargeHeaderArray = chargeHeader.keySet().toArray() /> + <#list chargeHeaderArray as currencyName> + ${currencyName} + + + + <#assign writeIndex = true /> <#-- 单元格合并后 第二行其实的单元格要指定起始列编号 --> + <#list chargeHeader.values() as chargeTypes> + <#list chargeTypes as type> + ss:Index="${bookingNoteHeader.size() + 1}">${type.substring(type.indexOf("-") + 1)} + <#assign writeIndex = false /> + + + + <#assign count = bookingNoteData.size() /> + <#if (count > 0)> + <#assign charges = chargeData.values().toArray() /> + <#list 0..(count-1) as i> + + <#assign bnData = bookingNoteData.get(i) /> + <#list bnData as label> + ${label} + + <#list chargeHeader.values() as chargeTypes> + <#list chargeTypes as type> + <#if charges[i].get( type )??> + ${charges[i].get( type )?string("0.00")} + <#else> + / + + + + + + + TOTAL: + <#assign chargeData = charges[count].values() /> + <#list chargeData as charge> + ${charge?string("0.00")} + + + <#-- 空行 --> + <#list chargeHeaderArray as currencyName> + + <#if currencyName_index == 0> + 合计: + + 0 )> ss:Index="2">${currencyName}: + ${charges[count].get( currencyName + "-小计" )?string("0.00")} + + + +
+ + + + + + True + + + + False + False + +
+
\ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account.ftl new file mode 100644 index 0000000..f6dc890 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account.ftl @@ -0,0 +1,60 @@ +<#-- 对账单 Excel 2003 模板 --> + + + + + + <#-- 对账单创建时间 --> + <#if model.customsDate??>${model.customsDate?string("yyyy-MM-ddTHH:mm:ssZ")} + + 11.6568<#-- Excel2003 版本 --> + + + + + + + False + False + + + + + + + <#if rows?? && rows?size > 0> + <#list rows as row> + ss:StyleID="${row.styleId}"<#if row.autoFitHeight??> ss:AutoFitHeight="${row.autoFitHeight}"<#if row.height??> ss:Height="${row.height}"> + <#if row.cells?? && row.cells?size > 0> + <#list row.cells as cell> + ss:StyleID="${cell.styleId}"<#if cell.mergeDown??> ss:MergeDown="${cell.mergeDown}"<#if cell.mergeAcross??> ss:MergeAcross="${cell.mergeAcross}"> + ss:Type="${row.type}">${cell.value!} + + + + + + +
+ + + + + + True + + + + False + False + +
+
diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account1.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account1.ftl new file mode 100644 index 0000000..ba75ae9 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/excel/account1.ftl @@ -0,0 +1,223 @@ + + + + + 14 + + + + + + + False + False + + + + + + + + + + + + + + + + + + + + + ${model.code!} + + + + + + + 客户: + + + ${model.target.name!} + + + 费用确认单 ${accountMonth!} + + + 杭州圣世特物流有限公司 杭州市文二路391号西湖国际科技大厦B3-1310 TEL:88473507 FAX:88473508 + + + + <#assign currentType="其他" /> + <#assign count = bookingNoteData.size() /> + <#if (count > 0)> + <#assign groupTotal = emptyTotalCharge> <#-- 获取空的分组统计 --> + <#assign charges = chargeData.values().toArray() /> + <#assign chargeHeaderArray = chargeHeader.keySet().toArray() /> + <#list 0..(count-1) as i> + <#assign bnData = bookingNoteData.get(i) /> + + <#-- 开始新的类型分组 --> + <#if bnData[0] != currentType> + <#-- 分组统计 --> + <#if currentType != "其他"> + + TOTAL: + <#list chargeHeader.values() as chargeTypes> + <#list chargeTypes as type> + ${groupTotal.get( type )?string("0.00")} + + + + <#-- 空行 --> + <#assign groupTotal = emptyTotalCharge> <#-- 获取空的分组统计 --> + + <#assign currentType=bnData[0] /> + + ${bnData[0]!}<#-- 海运出口/海运进口/空运出口/空运进口/国内运输 --> + + + <#list bookingNoteHeader as bnHeader> + ${bnHeader} + + <#list chargeHeaderArray as currencyName> + ${currencyName} + + + + <#assign writeIndex = true /> <#-- 单元格合并后 第二行起始的单元格要指定起始列编号 --> + <#list chargeHeader.values() as chargeTypes> + <#list chargeTypes as type> + ss:Index="${bookingNoteHeader.size() + 1}">${type.substring(type.indexOf("-") + 1)} + <#assign writeIndex = false /> + + + + + + <#-- 具体托单及费用数据 --> + + <#list bnData as label> + <#if label_index != 0>${label!} + + <#list chargeHeader.values() as chargeTypes> + <#list chargeTypes as type> + <#if charges[i].get( type )??> + ${charges[i].get( type )?string("0.00")} + <#-- 计算分组统计 --> + <#assign subtotal = charges[i].get( type ) + groupTotal.get( type ) />${groupTotal.put( type, subtotal )!} + <#else> + / + + + + + + + <#-- 分组统计 --> + + TOTAL: + <#list chargeHeader.values() as chargeTypes> + <#list chargeTypes as type> + ${groupTotal.get( type )?string("0.00")} + + + + <#-- 空行 --> + + <#-- 整体汇总 --> + <#list chargeHeaderArray as currencyName> + + <#if currencyName_index == 0> + 合计: + + 0 )> ss:Index="2">${currencyName}: + ${charges[count].get( currencyName + "-小计" )?string("0.00")} + <#if currencyName == "人民币">汇率<#else>${chargeRate.get( currencyName )?string("0.0000")} + + + + 折合人民币总计: + ${total?string("0.00")} + + +
+ + + + + + True + + + + False + False + +
+
\ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.ftl new file mode 100644 index 0000000..3e62fb7 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.ftl @@ -0,0 +1,59 @@ + + + +<#if model.direction == 1> + <#assign directionName = "结算单" /> + <#assign chargeStyle = "font-weight:bold;color:#BD362F;" /> +<#else> + <#assign directionName = "对账单" /> + <#assign chargeStyle = "font-weight:bold;color:#51A351;" /> + +${directionName} 【 ${model.code!} 】发票信息 + + + + +
+
+
+ <#-- 判断状态的静态类 --> +<#assign state = model.state?c /> +<#assign stateClass = "@com.ksa.model.finance.AccountState" /> +<#if stack.findValue("${stateClass}@isSettled(${state})")> + 状态:结算完毕 +<#elseif stack.findValue("${stateClass}@isChecked(${state})")> + 状态:<#if model.direction == 1>收款中<#else>付款中 + + +<#elseif stack.findValue("${stateClass}@isChecking(${state})")> + 状态:开票中 + +<#elseif stack.findValue("${stateClass}@isProcessing(${state})")> + 状态:审核中 + + +<#elseif stack.findValue("${stateClass}@isNone(${state})")> + <#if model.id??> + 状态:待审核 + + <#else> + 状态:新建 + + + +
+
<#-- end div --> + + \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.js b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.js new file mode 100644 index 0000000..3555a16 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/account/invoice.js @@ -0,0 +1,101 @@ +$(function(){ + var $grid = $('#data_grid').datagrid({ + url: ksa.buildUrl( "/data/finance/invoice", "query", { "ACCOUNT_ID" : ACCOUNT_ID } ), + fit : true, + pageSize: 20, + fitColumns: false, + columns:[ GET_INVOICE_TABLE_COLUMN( {"a.code":false} ) ], + toolbar : ( (STATE & 0x8) > 0) ? null : [{ + text:'添加新发票 ...', + cls:'btn-primary', + iconCls:'icon-plus icon-white', + handler:function(){ + top.$.open({ + width:650, + height:400, + + title: "新建发票", + src : ksa.buildUrl( "/dialog/finance/invoice", "create", { direction: DIRECTION * -1, "account.id": ACCOUNT_ID, "target.id": TARGET_ID } ) + }, function(){ + $grid.datagrid( "reload" ); + }); + } + }, '-', { + text:'选择已有发票 ...', + cls:'btn-success', + iconCls:'icon-search icon-white', + handler:function(){ + ksa.finance.selectInvoices( function( results ) { + $.each( results, function(i, v){ + var invoice = v.data; + if( invoice.account.id == ACCOUNT_ID ) { + return; + } else if( invoice.direction == DIRECTION ) { + top.$.messager.warning(DIRECTOIN_NAME + "必须选择" + ( DIRECTION == 1 ? "开出" : "收到" ) + "的发票进行对账销账。"); + } else if( invoice.account.id ) { + top.$.messager.warning("发票【"+invoice.code+"】已经被用于对账销账,无法重复使用。"); + } else { + assignInvoice( invoice.id, ACCOUNT_ID, (i+1) == results.length ); // 最后一个提交的刷新列表 + } + } ); + }, { direction : DIRECTION * -1, settle: "false"} ); + } + }, { + text:'销账还原', + cls:'btn-danger', + float:'right', + iconCls:'icon-share-alt icon-white', + handler:function(){ + var row = $grid.datagrid( "getSelected" ); + if( ! row ) { + top.$.messager.warning("请选择一条数据后,再进行还原操作。"); + return; + } + // 还原 + assignInvoice( row.id, "", true ); + } + }] + }); + + function assignInvoice( invoiceId, accountId, reload ) { + $.ajax({ + url: ksa.buildUrl( "/dialog/finance/invoice", "assign" ), + data: { "id" : invoiceId, "account.id" : accountId }, + success: function( result ) { + try { + if (result.status == "success" && reload ) { + $grid.datagrid("reload"); + } + else { top.$.messager.error( result.message ); } + } catch (e) { } + } + }); + }; + + // ------------- 状态变更操作 + $("#go_processing").click(function(){ changeState(1, "确定提交审核"+ACCOUNT_NAME+"吗?
账单提交审核后将不能修改【汇率】信息。"); return false; }); + $("#go_checking").click(function(){ changeState(2, "确定"+ACCOUNT_NAME+"通过审核吗?"); return false; }); + $("#return_processing").click(function(){ changeState(0, "确定将"+ACCOUNT_NAME+"打回进行修正吗?"); return false; }); + $("#go_checked").click(function(){ changeState(8, "确定完成发票开具,进行"+(DIRECTION == 1 ? "收款" : "付款")+"确认吗?"); return false; }); + $("#return_checked").click(function(){ changeState(2, "确定重新开具发票吗?"); return false; }); + $("#go_settled").click(function(){ changeState(32, "确定"+ACCOUNT_NAME+"结算完毕吗?"); }); + function changeState( newState, message ) { + parent.$.messager.confirm( message, function( ok ){ + if( ok ) { + $.ajax({ + url: ksa.buildUrl( "/dialog/finance/account", "state" ), + data: { id : ACCOUNT_ID, state : newState }, + success: function( result ) { + try { + if (result.status == "success") { + parent.window.location.reload(); + } + else { parent.$.messager.error( result.message ); } + } catch (e) { } + } + }); + } + } ); + return false; + } +}); \ No newline at end of file diff --git a/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/debitNote.ftl b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/debitNote.ftl new file mode 100644 index 0000000..0628f97 --- /dev/null +++ b/test_input/ksa/ksa-web-root/ksa-finance-web/src/main/resources/ui/finance/business/debitNote.ftl @@ -0,0 +1,2400 @@ + + + + + 14.00 + + + 2052-8.1.0.2998 + + + + + + 6510 + 11985 + 0 + 30 + False + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Room915-B4,Xi hu International Technology Building,391 Wener Rd,Xihu District, Hangzhou, China Tel:0571-8847-3507 Fax:0571-8847-3508 + + + + 凯思爱(杭州)物流有限公司 + + + + KSA LOGISTICS CENTER (HANGZHOU) CO.,LTD + + + + + + + + + + + + + + + + + + + DEBIT NOTE + + NO.: + ${model.code!} + + + + + + + + + + + + DATE: + ${model.createdDate?date} + + + ***** + + + + + + + + + + + + + TOTAL : + + + + + + + + + + + + + + + + + SHIPMENT + N/A + + + FROM/TO + FROM: ${model.departurePort!} + TO: + ${model.destinationPort!} + + + + INSURANCE + + None + + + TIME OF JOB + + + + + + + + CATEGORY + + EXP   + + + IMP     + + + + + + + + JOB + + + + + + DESCRIPTION + + + + + + + CONTENTS + Q'ty + PKGS + WGT + VOL + + + + SHIPMENT + + ${model.cargoName!} + ${model.cargoQuantity!} + ${model.cargoName!} + ${model.cargoWeight!} + ${model.cargoVolumn!} + + + + + + + + + + + + + + SPECIAL INSTRUCTION + + + <#if model.shipper??>${model.shipper.alias!} + + + + + SHIPPER: + + + + + + CONSIGNEE: + + <#if model.consignee??>${model.consignee.alias!} + + + + + + + + + + I T E M + UNIT + DESCRIPTION + AMOUNT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${charge[0].item!} + ${charge[0].unit!} + ${charge[0].note!} + ${charge[0].amount!} + + + + + ${charge[1].item!} + ${charge[1].unit!} + ${charge[1].note!} + ${charge[1].amount!} + + + + + ${charge[2].item!} + ${charge[2].unit!} + ${charge[2].note!} + ${charge[2].amount!} + + + + + ${charge[3].item!} + ${charge[3].unit!} + ${charge[3].note!} + ${charge[3].amount!} + + + + + ${charge[4].item!} + ${charge[4].unit!} + ${charge[4].note!} + ${charge[4].amount!} + + + + + ${charge[5].item!} + ${charge[5].unit!} + ${charge[5].note!} + ${charge[5].amount!} + + + + + ${charge[6].item!} + ${charge[6].unit!} + ${charge[6].note!} + ${charge[6].amount!} + + + + + ${charge[7].item!} + ${charge[7].unit!} + ${charge[7].note!} + ${charge[7].amount!} + + + + + ${charge[8].item!} + ${charge[8].unit!} + ${charge[8].note!} + ${charge[8].amount!} + + + + + ${charge[9].item!} + ${charge[9].unit!} + ${charge[9].note!} + ${charge[9].amount!} + + + + + ${charge[10].item!} + ${charge[10].unit!} + ${charge[10].note!} + ${charge[10].amount!} + + + + + ${charge[11].item!} + ${charge[11].unit!} + ${charge[11].note!} + ${charge[11].amount!} + + + + + + + + + + TOTAL: + USD + ${total?c} + + + + + Please remit to : + + + + + + + + + + + + + SUMITOMO MITSUI BANKING CORPORATION ,HANGZHOU BRANCH + + + + + + + + + + + + + 23F,Golden Plaza,118Qing Chun Road ,Xia Cheng District ,Hangzhou,Zhejiang + + + + + + + + + + + + + 310003,People 's Republic of China + + + + + + + + + + + + + A/C No.: + 1300027-1(USD) + + + + + + + + + + + + + BA1300027-1(JPY) + + + + + + SIGNATURE + + + + + AGRICULTURE BANK OF CHINA ,HANGZHOU CITY YANAN BRANCH + + + + + + + + + + + + + No521,Yanan Road ,Hangzhou ,Zhejiang,310006,People 's Republic of China + + + + + + + + + + + + A/C No.:036101040012779(RMB) + + + + + + + + + + +   + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+