From 6268518f8ccfe3091b70fc7416d5bc4818502940 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Sat, 21 Oct 2017 13:30:26 +0300 Subject: [PATCH] usages-3.0-alpha-1 --- .gitignore | 47 ++ HEADER.txt | 15 + LICENSE.txt | 674 ++++++++++++++++++ README.md | 49 ++ aether-shaded/build.gradle | 15 + api/build.gradle | 7 + .../kotlin/com/devexperts/usages/api/Api.kt | 186 +++++ .../kotlin/com/devexperts/usages/api/Model.kt | 208 ++++++ build.gradle | 95 +++ console-client/build.gradle | 11 + .../usages/cclient/ConsoleClient.kt | 77 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++ gradlew.bat | 84 +++ idea-plugin/build.gradle | 20 + .../FindUsagesRequestConfigurationPanel.form | 167 +++++ ...UsagesRequestConfigurationPanelHolder.java | 198 +++++ .../idea/PluginConfigurationDialogPanel.form | 86 +++ .../PluginConfigurationDialogPanelHolder.java | 133 ++++ .../usages/idea/ServerAddressPanel.form | 38 + .../usages/idea/ServerAddressPanelHolder.java | 72 ++ .../usages/idea/SourceRepositoryPanel.form | 64 ++ .../idea/SourceRepositoryPanelHolder.java | 86 +++ .../com/devexperts/usages/idea/FindUsages.kt | 104 +++ .../usages/idea/FindUsagesRequestAction.kt | 97 +++ .../idea/FindUsagesRequestConfiguration.kt | 133 ++++ .../usages/idea/PluginConfiguration.kt | 151 ++++ .../com/devexperts/usages/idea/UsagesTree.kt | 286 ++++++++ .../devexperts/usages/idea/UsagesViewer.kt | 138 ++++ .../src/main/resources/META-INF/plugin.xml | 61 ++ .../src/main/resources/icons/find_usages.png | Bin 0 -> 3876 bytes server/build.gradle | 63 ++ .../devexperts/usages/analyzer/Analyzer0.java | 210 ++++++ .../usages/analyzer/AnalyzerAppConfig.java | 33 + .../usages/analyzer/ApiScanResult.java | 48 ++ .../usages/analyzer/ApiScanner.java | 66 ++ .../com/devexperts/usages/analyzer/Cache.java | 45 ++ .../usages/analyzer/ClassUsages.java | 398 +++++++++++ .../usages/analyzer/ClassUsagesAnalyzer.java | 358 ++++++++++ .../devexperts/usages/analyzer/Config.java | 77 ++ .../devexperts/usages/analyzer/Constants.java | 23 + .../devexperts/usages/analyzer/FileUtils.java | 46 ++ .../com/devexperts/usages/analyzer/Fmt.java | 30 + .../usages/analyzer/MainAnalyzer.java | 65 ++ .../usages/analyzer/Processing.java | 61 ++ .../devexperts/usages/analyzer/Processor.java | 25 + .../devexperts/usages/analyzer/PublicApi.java | 144 ++++ .../usages/analyzer/PublicApiAnalyzer.java | 65 ++ .../analyzer/SmartDirectoriesWalker.java | 105 +++ .../com/devexperts/usages/analyzer/Usage.java | 109 +++ .../devexperts/usages/analyzer/Usages.java | 214 ++++++ .../usages/analyzer/UsagesScanResult.java | 88 +++ .../usages/analyzer/UsagesScanner.java | 78 ++ .../devexperts/usages/analyzer/UseKind.java | 52 ++ .../concurrent/ConcurrentOutputStream.java | 126 ++++ .../executors/ActionExecutionException.java | 44 ++ .../executors/CallerInterruptedException.java | 31 + .../executors/DaemonThreadFactory.java | 52 ++ .../analyzer/executors/InsistentExecutor.java | 358 ++++++++++ .../analyzer/executors/NullSemaphore.java | 100 +++ .../analyzer/internal/MemberInternal.java | 111 +++ .../tune/AnalyzeSingleUsagesKeeper.java | 55 ++ .../analyzer/tune/SimpleUsagesKeeper.java | 60 ++ .../usages/analyzer/tune/UsagesKeeper.java | 31 + .../usages/analyzer/walker/FileAnalyzer.java | 26 + .../usages/analyzer/walker/info/FileInfo.java | 36 + .../analyzer/walker/info/FileInfoBase.java | 47 ++ .../analyzer/walker/info/PlainFileInfo.java | 54 ++ .../analyzer/walker/info/ZipEntryInfo.java | 55 ++ .../analyzer/walker/walkers/Delegating.java | 30 + .../walker/walkers/DelegatingWalker.java | 35 + .../analyzer/walker/walkers/EmptyWalker.java | 29 + .../walker/walkers/FilePackWalker.java | 67 ++ .../walker/walkers/MidDelegating.java | 31 + .../analyzer/walker/walkers/PackWalker.java | 42 ++ .../analyzer/walker/walkers/Passing.java | 35 + .../walker/walkers/TerminalWalker.java | 57 ++ .../analyzer/walker/walkers/Walker.java | 34 + .../walker/walkers/ZipRecursiveWalker.java | 103 +++ .../com/devexperts/usages/server/Database.kt | 133 ++++ .../com/devexperts/usages/server/Server.kt | 187 +++++ .../usages/server/analyzer/Analyzer.kt | 57 ++ .../usages/server/analyzer/ClassAnalyzer.kt | 115 +++ .../usages/server/analyzer/UsagesManager.kt | 127 ++++ .../server/artifacts/ArtifactManager.kt | 251 +++++++ .../usages/server/config/Configuration.kt | 51 ++ .../usages/server/config/Settings.kt | 106 +++ .../usages/server/indexer/IndexerCli.kt | 70 ++ .../usages/server/indexer/MavenIndexer.kt | 80 +++ .../server/indexer/NexusMavenIndexer.kt | 275 +++++++ .../devexperts/usages/server/TestElements.kt | 57 ++ .../server/analyzer/UsagesManagerTest.kt | 94 +++ .../server/artifacts/ArtifactManagerTest.kt | 85 +++ 94 files changed, 9220 insertions(+) create mode 100644 .gitignore create mode 100644 HEADER.txt create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 aether-shaded/build.gradle create mode 100644 api/build.gradle create mode 100644 api/src/main/kotlin/com/devexperts/usages/api/Api.kt create mode 100644 api/src/main/kotlin/com/devexperts/usages/api/Model.kt create mode 100644 build.gradle create mode 100644 console-client/build.gradle create mode 100644 console-client/src/main/kotlin/com/devexperts/usages/cclient/ConsoleClient.kt create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 idea-plugin/build.gradle create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanel.form create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanelHolder.java create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanel.form create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanelHolder.java create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanel.form create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanelHolder.java create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanel.form create mode 100644 idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanelHolder.java create mode 100644 idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsages.kt create mode 100644 idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestAction.kt create mode 100644 idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestConfiguration.kt create mode 100644 idea-plugin/src/main/kotlin/com/devexperts/usages/idea/PluginConfiguration.kt create mode 100644 idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesTree.kt create mode 100644 idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesViewer.kt create mode 100644 idea-plugin/src/main/resources/META-INF/plugin.xml create mode 100644 idea-plugin/src/main/resources/icons/find_usages.png create mode 100644 server/build.gradle create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Analyzer0.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/AnalyzerAppConfig.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/ApiScanResult.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/ApiScanner.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Cache.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/ClassUsages.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/ClassUsagesAnalyzer.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Config.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Constants.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/FileUtils.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Fmt.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/MainAnalyzer.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Processing.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Processor.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/PublicApi.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/PublicApiAnalyzer.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/SmartDirectoriesWalker.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Usage.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/Usages.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/UsagesScanResult.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/UsagesScanner.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/UseKind.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/concurrent/ConcurrentOutputStream.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/executors/ActionExecutionException.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/executors/CallerInterruptedException.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/executors/DaemonThreadFactory.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/executors/InsistentExecutor.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/executors/NullSemaphore.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/internal/MemberInternal.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/tune/AnalyzeSingleUsagesKeeper.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/tune/SimpleUsagesKeeper.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/tune/UsagesKeeper.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/FileAnalyzer.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfo.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfoBase.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/info/PlainFileInfo.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/info/ZipEntryInfo.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Delegating.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/DelegatingWalker.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/EmptyWalker.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/FilePackWalker.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/MidDelegating.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/PackWalker.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Passing.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/TerminalWalker.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Walker.java create mode 100644 server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/ZipRecursiveWalker.java create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/Database.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/Server.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/analyzer/Analyzer.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/analyzer/ClassAnalyzer.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/analyzer/UsagesManager.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/artifacts/ArtifactManager.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/config/Configuration.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/config/Settings.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/indexer/IndexerCli.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/indexer/MavenIndexer.kt create mode 100644 server/src/main/kotlin/com/devexperts/usages/server/indexer/NexusMavenIndexer.kt create mode 100644 server/src/test/kotlin/com/devexperts/usages/server/TestElements.kt create mode 100644 server/src/test/kotlin/com/devexperts/usages/server/analyzer/UsagesManagerTest.kt create mode 100644 server/src/test/kotlin/com/devexperts/usages/server/artifacts/ArtifactManagerTest.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceb870b --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +#idea files +.idea/ +*.iml +*.ipr +*.iws +atlassian-ide-plugin.xml +out/ + +# java +*.class +*.jar +*.war +*.ear + +# maven specific +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +#gradle +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +# common +*~ +*.bak +.DS_Store +.DS_Store? +._* diff --git a/HEADER.txt b/HEADER.txt new file mode 100644 index 0000000..a79272b --- /dev/null +++ b/HEADER.txt @@ -0,0 +1,15 @@ +Copyright (C) ${year} ${name} + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public +License along with this program. If not, see +. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3efceeb --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +Usages Analysis Tool +====== + +This tool finds code usages in the specified Maven repositories. It indexes repositories, downloads required artifacts and scans ".class" files in them. The tool analyzes all kinds of dependencies: usages of fields, usages of methods, extensions of classes and implementation of interfaces, usages of annotations, overrides of methods. + +The tool is separated into 2 parts: server application, which collects all information and analyzes classes, and a client one, which is implemented as IntelliJ Plugin. Both of them are currently in development, but alpha versions are available on our portal. +[https://code.devexperts.com/display/USAGES/About+Usages]() + +Server +------ +Server part of the tool is a Java application and could be run simply: + +`java -jar server-${version}.jar` + + +### Configuration properties +All parameters are passed as system variables (`-D=`) + +* **server.port** - defines server port, which is used for find usages requests, *8080* by default; +* **usages.workDir** - defines working directory for *settings.xml* (see section below) and work files (e.g. caches, database), *~/usages/* by default. + +### Repositories indexing configuration +You need to provide information about your repositories in `${usages.workDir}/settings.xml` file. See the example below. + +```xml + + + nexus + username + password + + + jar + war + + +``` + +IntelliJ Plugin +--------------- +Plugin for IntelliJ IDEA should be installed from disk using `idea-plugin-${version}.jar`. After the installation, set URL to usages server in plugin configuration (**Tools --> Configure Usages plugin**). To perform find usages action use **CTRL + F9** (for simple search) or **ALT + CTRL + SHIFT + F9** (with configuration before the search) shortcut when you are on the element to be searched for. diff --git a/aether-shaded/build.gradle b/aether-shaded/build.gradle new file mode 100644 index 0000000..781aeae --- /dev/null +++ b/aether-shaded/build.gradle @@ -0,0 +1,15 @@ +dependencies { + compile "org.apache.maven:maven-aether-provider:3.3.9" + compile "org.eclipse.aether:aether-api:$aether_version" + compile "org.eclipse.aether:aether-util:$aether_version" + compile "org.eclipse.aether:aether-transport-file:$aether_version" + compile "org.eclipse.aether:aether-transport-http:$aether_version" + compile "org.eclipse.aether:aether-connector-basic:$aether_version" +} + +apply plugin: 'com.github.johnrengelman.shadow' + +shadowJar { + relocate 'com.google', 'com.devexperts.usages.shaded.com.google' + relocate 'org.objectweb.asm', 'com.devexperts.usages.shaded.org.objectweb.asm' +} \ No newline at end of file diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..e56f1f4 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,7 @@ +description = 'Internal API' +dependencies { +// compile group: 'org.springframework', name: 'spring-web', version: '4.3.2.RELEASE' + compile "org.springframework.boot:spring-boot-starter-webflux:$spring_boot_version" + compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.7.2' + compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.2' +} diff --git a/api/src/main/kotlin/com/devexperts/usages/api/Api.kt b/api/src/main/kotlin/com/devexperts/usages/api/Api.kt new file mode 100644 index 0000000..0a03f2f --- /dev/null +++ b/api/src/main/kotlin/com/devexperts/usages/api/Api.kt @@ -0,0 +1,186 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.api + +import com.fasterxml.jackson.annotation.JsonProperty +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import java.lang.Integer.parseInt +import java.util.* + +const val UUID_HEADER_NAME = "UUID" + +/** + * It is used to do [MemberUsageRequest] and process responses in stream format. + * Note that all methods can be invoked concurrently, from different threads. + */ +abstract class MemberUsageRequestProcessor(val serverUrls: List, + val memberUsagesRequest: MemberUsageRequest) { + private val uuid = UUID.randomUUID().toString() + private @Volatile + var cancelled = false + abstract fun onNewUsages(serverUrl: String, usages: List) + abstract fun onError(serverUrl: String, message: String, throwable: Throwable?) + abstract fun onComplete() + + fun doRequest() { + serverUrls.forEach { url -> + try { + val webClient = WebClient.builder() + .baseUrl(url) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_STREAM_JSON_VALUE) + .defaultHeader(UUID_HEADER_NAME, uuid) + .build() + val usagesFlux = webClient.post() + .uri("/usages") + .body(BodyInserters.fromObject(memberUsagesRequest)) + .exchange().flatMapMany { it.bodyToFlux(MemberUsage::class.java) } + usagesFlux.toStream().forEach{ onNewUsages(url, listOf(it)) } +// val block = CountDownLatch(1) +// usagesFlux.doFinally { block.countDown() } +// block.await() +// usagesFlux.subscribe(object : Subscriber { +// override fun onNext(usage: MemberUsage) { +// onNewUsages(url, listOf(usage)) +// } +// +// override fun onSubscribe(s: Subscription) { +// subscriptions += s +// } +// +// override fun onError(t: Throwable) { +// block.countDown() +// onError(url, "Error during request to $url", t) +// } +// +// override fun onComplete() { +// block.countDown() +// } +// }) +// block.await() + } catch (e: Throwable) { + onError(url, "Error during request to $url", e) + } + } + onComplete() + } + + fun cancel() { + cancelled = true + } +} + +enum class CompleteMessage { + COMPLETE, USAGES_NUMBER_EXCEED +} + + +/** + * Parses artifact mask which should be in one of the following formats: + * * `::` + * * `:::` + * * `:::::` + */ +fun createArtifactMaskFromString(config: String): ArtifactMask { + val props = config.trim().split(":") + if (props.size == 3) { + // :: + return ArtifactMask(groupId = props[0], artifactId = props[1], version = props[2]) + } else if (props.size == 4) { + // ::: + return ArtifactMask(groupId = props[0], artifactId = props[1], version = props[2], + numberOfLastVersions = parseInt(props[3])) + } else if (props.size == 6) { + // ::::: + return ArtifactMask(groupId = props[0], artifactId = props[1], packaging = props[2], + classifier = props[3], version = props[4], numberOfLastVersions = parseInt(props[5])) + } else { + throw IllegalArgumentException("Invalid configuration: " + config) + } +} + +const val ANY = "*" + +data class ArtifactMask( + @JsonProperty("groupId") val groupId: String = ANY, + @JsonProperty("artifactId") val artifactId: String = ANY, + @JsonProperty("packaging") val packaging: String = ANY, + @JsonProperty("classifier") val classifier: String = ANY, + @JsonProperty("version") val version: String = ANY, + @JsonProperty("numberOfLastVersions") val numberOfLastVersions: Int = 1 +) + +data class MemberUsageRequest( + @JsonProperty("member") val member: Member, // find usages of this member + @JsonProperty("searchScope") val searchScope: ArtifactMask, // search in these artifacts only + @JsonProperty("findClasses") val findClasses: Boolean, // find usages of classes from specified package (as member) + @JsonProperty("findMethods") val findMethods: Boolean, // find method usages during finding usages of the class + @JsonProperty("findFields") val findFields: Boolean, +// @JsonProperty("findBaseClassesUsages") val findBaseClassesUsages: Boolean, + @JsonProperty("findDerivedClassesUsages") val findDerivedClassesUsages: Boolean, +// @JsonProperty("findBaseMethodsUsages") val findBaseMethodsUsages: Boolean, + @JsonProperty("findDerivedMethodsUsages") val findDerivedMethodsUsages: Boolean +) + + +val usages = arrayListOf( + MemberUsage( + member = Member("com.devexperts.util.TimePeriod", emptyList(), MemberType.CLASS), + usageKind = UsageKind.FIELD, // property type + location = Location( + artifact = Artifact("com.devexperts.usages", "server", "3.0", null, null), + member = Member("com.devexperts.usages.config.RepositorySettings", emptyList(), MemberType.CLASS), + file = "com/devexperts/usages/config/Settings.kt", lineNumber = 54)), + MemberUsage( + member = Member("com.devexperts.util.TimePeriod", emptyList(), MemberType.CLASS), + usageKind = UsageKind.METHOD_RETURN, // method return type + location = Location( + artifact = Artifact("com.devexperts.usages", "server", "3.0", null, null), + member = Member("com.devexperts.usages.config.TimePeriodConverter#myMethod", emptyList(), MemberType.METHOD), + file = "com/devexperts/usages/config/Settings.kt", lineNumber = 76)), + MemberUsage( + member = Member("com.devexperts.util.TimePeriod", emptyList(), MemberType.CLASS), + usageKind = UsageKind.METHOD_RETURN, // nested class/object + location = Location( + artifact = Artifact("com.devexperts.usages", "server", "3.0", null, null), + member = Member("com.devexperts.usages.config.TimePeriodConverter", emptyList(), MemberType.CLASS), + file = "com/devexperts/usages/config/Settings.kt", lineNumber = 77)), + MemberUsage( + member = Member("com.devexperts.util.TimePeriod", emptyList(), MemberType.CLASS), + usageKind = UsageKind.METHOD_RETURN, // nested class/object + location = Location( + artifact = Artifact("com.devexperts.usages", "server", "3.0", null, null), + member = Member("com.devexperts.usages.config.RepositorySettings", emptyList(), MemberType.CLASS), + file = "com/devexperts/usages/config/Settings.kt", lineNumber = 54)), + MemberUsage( + member = Member("com.devexperts.util.IndexedSet", emptyList(), MemberType.CLASS), + usageKind = UsageKind.NEW, + location = Location( + artifact = Artifact("com.devexperts.qd", "qd-core", "3.257", null, null), + member = Member("com.devexperts.qd.core.MyIndexedSet", emptyList(), MemberType.CLASS), + file = "com/devexperts/qd/core/MyIndexedSet", lineNumber = 47)), + MemberUsage( + member = Member("com.devexperts.util.IndexedSet", emptyList(), MemberType.CLASS), + usageKind = UsageKind.FIELD, + location = Location( + artifact = Artifact("com.devexperts.qd", "qd-core", "3.256", null, null), + member = Member("com.devexperts.qd.core.MyIndexedSet", emptyList(), MemberType.CLASS), + file = "com/devexperts/qd/core/MyIndexedSet", lineNumber = 35)) +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/devexperts/usages/api/Model.kt b/api/src/main/kotlin/com/devexperts/usages/api/Model.kt new file mode 100644 index 0000000..7867efa --- /dev/null +++ b/api/src/main/kotlin/com/devexperts/usages/api/Model.kt @@ -0,0 +1,208 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.api + +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +data class Member( + @JsonProperty("memberName") val qualifiedMemberName: String, + @JsonProperty("paramTypes") val parameterTypes: List, + @JsonProperty("type") val type: MemberType +) { + + fun packageName(): String { + if (type == MemberType.PACKAGE) + return qualifiedMemberName; + val className = className(); + val lastDotIndex = className.lastIndexOf('.') + return if (lastDotIndex == -1) qualifiedMemberName else qualifiedMemberName.substring(0, lastDotIndex) + } + + fun className() = when (type) { + MemberType.PACKAGE -> throw IllegalStateException("Cannot return class name for package member") + MemberType.CLASS -> qualifiedMemberName + else -> qualifiedMemberName.substring(0, qualifiedMemberName.indexOf('#')) + } + + fun simpleClassName() = simpleNonPackageName(className()) + + fun simpleName(): String { + if (type == MemberType.PACKAGE) + return qualifiedMemberName + var simpleName = simpleNonPackageName(qualifiedMemberName) + if (type == MemberType.METHOD) + simpleName += "(${simpleParameters()})" + return simpleName + } + + fun simpleMemberName(): String { + if (type == MemberType.PACKAGE || type == MemberType.CLASS) + throw IllegalStateException("Simple member name is allowed for fields and methods only, current member is $this") + var simpleMemberName = qualifiedMemberName.substring(qualifiedMemberName.indexOf("#") + 1) + if (type == MemberType.METHOD && !parameterTypes.isEmpty()) + simpleMemberName += "(${simpleParameters()})" + return simpleMemberName + } + + private fun simpleNonPackageName(qualifiedName: String): String { + val lastDotIndex = qualifiedName.lastIndexOf('.') + return if (lastDotIndex < 0) qualifiedName else qualifiedName.substring(lastDotIndex + 1) + } + + private fun simpleParameters(): String { + val paramsJoiner = StringJoiner(", ") + for (p in parameterTypes) { + paramsJoiner.add(simpleNonPackageName(p)) + } + return paramsJoiner.toString() + } + + override fun toString(): String { + var res = qualifiedMemberName + if (type == MemberType.METHOD) { + res = "$res(${simpleParameters()})" + } + return res + } + + override fun hashCode(): Int { + var result = qualifiedMemberName.hashCode() + result = 31 * result + parameterTypes.hashCode() + result = 31 * result + type.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as Member + + if (qualifiedMemberName != other.qualifiedMemberName) return false + if (parameterTypes != other.parameterTypes) return false + if (type != other.type) return false + + return true + } + + companion object { + fun fromPackage(packageName: String) = Member(packageName, emptyList(), MemberType.PACKAGE) + fun fromClass(className: String) = Member(className, emptyList(), MemberType.CLASS) + fun fromField(className: String, fieldName: String) = Member( + "$className#$fieldName", emptyList(), MemberType.FIELD) + fun fromMethod(className: String, methodName: String, parameterTypes: List) = Member( + "$className#$methodName", parameterTypes, MemberType.METHOD) + } +} + + +enum class MemberType constructor(val typeName: String) { + PACKAGE("package"), + CLASS("class"), + METHOD("method"), + FIELD("field"); + + override fun toString(): String { + return typeName + } +} + + +data class MemberUsage( + @JsonProperty("member") val member: Member, + @JsonProperty("usageKind") val usageKind: UsageKind, + @JsonProperty("location") val location: Location +) + + +data class Location( + @JsonProperty("artifact") val artifact: Artifact, + @JsonProperty("member") val member: Member, // class or method + @JsonProperty("file") val file: String?, // content file, do not use it for equals and hashCode! + @JsonProperty("line") val lineNumber: Int? // line number of usage in the file +) { + init { +// if (member.type != MemberType.CLASS && member.type != MemberType.METHOD) +// throw IllegalArgumentException("Only methods and classes could be used as location, current member $member") + } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as Location + + if (artifact != other.artifact) return false + if (member != other.member) return false + if (lineNumber != other.lineNumber) return false + + return true + } + + override fun hashCode(): Int { + var result = artifact.hashCode() + result = 31 * result + member.hashCode() + if (lineNumber != null) + result = 31 * result + lineNumber + return result + } + + +} + + +data class Artifact( + @JsonProperty("groupId") val groupId: String, + @JsonProperty("artifactId") val artifactId: String, + @JsonProperty("version") val version: String, + @JsonProperty("type") val type: String?, + @JsonProperty("classifier") val classifier: String? +) { + override fun toString() = "$groupId:$artifactId:${type ?: ""}:${classifier ?: ""}:$version" +} + + +// Do not rename enum values for backwards compatibility +enum class UsageKind constructor(val description: String) { + UNCLASSIFIED("Unclassified"), + SIGNATURE("Signature"), + CLASS_DECLARATION("Class declaration"), + EXTEND_OR_IMPLEMENT("Usage in inheritance (extends or implements)"), + OVERRIDE("Method overriding"), + METHOD_DECLARATION("Method declaration"), + METHOD_PARAMETER("Method parameter"), + METHOD_RETURN("Method return type"), + ANNOTATION("Annotation"), + THROW("Throw"), + CATCH("Catch"), + CONSTANT("Constant"), + FIELD("Field"), + ASTORE("Local variable"), + NEW("New instance"), + ANEWARRAY("New array"), + CAST("Type cast"), + GETFIELD("Read field"), + PUTFIELD("Write field"), + GETSTATIC("Read static field"), + PUTSTATIC("Write static field"), + INVOKEVIRTUAL("Invoke virtual method"), + INVOKESPECIAL("Invoke special method"), + INVOKESTATIC("Invoke static method"), + INVOKEINTERFACE("Invoke interface method"), + INVOKEDYNAMIC("Invoke dynamic") +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..8617844 --- /dev/null +++ b/build.gradle @@ -0,0 +1,95 @@ +buildscript { + ext.kotlin_version = '1.1.3-2' + + repositories { + maven { url "https://plugins.gradle.org/m2/" } + mavenCentral() + jcenter() + } + + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" +// classpath "gradle.plugin.nl.javadude.gradle.plugins:license-gradle-plugin:0.14.0" + } +} + +plugins { +// id "com.github.hierynomus.license" version "0.14.0" +// id "com.github.hierynomus.license-base" version "0.14.0" +} + +allprojects { + group = 'com.devexperts.usages' + version = '3.0-alpha-1' +} + +ext { + aether_version = "1.0.0.v20140518" + dxlib_version = "3.255" +// spring_version = "5.0.0.RELEASE" + spring_boot_version = "2.0.0.M4" +} + +//apply plugin: 'license' + +//license { +// header = project.file('HEADER.txt') +// ext.year = Calendar.getInstance().get(Calendar.YEAR) +// ext.name = 'Devexperts LLC' +//} + +subprojects { + apply plugin: 'java' + apply plugin: 'kotlin' +// apply plugin: 'license' + +// license { +// header = rootProject.file('HEADER.txt') +// ext.year = Calendar.getInstance().get(Calendar.YEAR) +// ext.name = 'Devexperts LLC' +// } + + + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + } + + repositories { + jcenter() + mavenCentral() + maven { url "http://repo.spring.io/milestone/" } + maven { url "https://dl.bintray.com/dxfeed/Maven/" } + maven { url "https://dl.bintray.com/kotlin/exposed" } + } + + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.16" + } + + kotlin { + experimental { + coroutines "enable" + } + } + + compileKotlin { + kotlinOptions.jvmTarget = "1.8" + } + + compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" + } + + buildscript { + repositories { + jcenter() + mavenCentral() + maven { url "http://repo.spring.io/milestone/" } + } + } +} diff --git a/console-client/build.gradle b/console-client/build.gradle new file mode 100644 index 0000000..d3fb9f1 --- /dev/null +++ b/console-client/build.gradle @@ -0,0 +1,11 @@ +dependencies { + compile project(':api') +} + +jar { + manifest { + attributes "Main-Class": "com.devexperts.usages.cclient.ConsoleClientKt" + } + // Create Uber JAR + from configurations.compile.collect { zipTree(it)} +} \ No newline at end of file diff --git a/console-client/src/main/kotlin/com/devexperts/usages/cclient/ConsoleClient.kt b/console-client/src/main/kotlin/com/devexperts/usages/cclient/ConsoleClient.kt new file mode 100644 index 0000000..41a02a3 --- /dev/null +++ b/console-client/src/main/kotlin/com/devexperts/usages/cclient/ConsoleClient.kt @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.cclient + +import com.devexperts.usages.api.* + +fun main(args: Array) { + val memberType = MemberType.valueOf(getRequiredStringPropertyOrExit("memberType").toUpperCase()) + val memberDesc = getRequiredStringPropertyOrExit("member") + val servers = getRequiredStringPropertyOrExit("servers").split(";") + + val member = when (memberType) { + MemberType.PACKAGE -> Member(memberDesc, emptyList(), memberType) + MemberType.CLASS -> Member(memberDesc, emptyList(), memberType) + MemberType.FIELD -> Member(memberDesc, emptyList(), memberType) + MemberType.METHOD -> { + val openBracketIndex = memberDesc.indexOf('('); + val qualifiedMemberName = memberDesc.substring(0, openBracketIndex) + val parameters = memberDesc.substring(openBracketIndex + 1, memberDesc.length - 1).split(",") + Member(qualifiedMemberName, parameters, memberType) + } + } + val request = MemberUsageRequest(member = member, + findClasses = getBooleanProperty("findClasses", true), + findDerivedClassesUsages = getBooleanProperty("findDerivedClassesUsages", true), + findFields = getBooleanProperty("findFields", true), + findMethods = getBooleanProperty("findMethods", true), + findDerivedMethodsUsages = getBooleanProperty("findDerivedMethodsUsages", true), + searchScope = createArtifactMaskFromString(getStringProperty("searchScope", "*:*:*")) + ) + val requestProcessor = object : MemberUsageRequestProcessor(servers, request) { + override fun onNewUsages(serverUrl: String, usages: List) { + usages.forEach { println(it) } + } + + override fun onError(serverUrl: String, message: String, throwable: Throwable?) { + println("ERROR: $message") + } + + override fun onComplete() { + println("COMPLETED") + } + } + requestProcessor.doRequest() +} + +private fun getBooleanProperty(key: String, defaultValue: Boolean): Boolean { + return System.getProperty(key)?.toBoolean() ?: defaultValue; +} + +private fun getStringProperty(key: String, defaultValue: String): String { + return System.getProperty(key) ?: defaultValue; +} + +private fun getRequiredStringPropertyOrExit(key: String): String { + val value = System.getProperty(key) + if (value == null) { + println("Property $key should be specified") + System.exit(0) + } + return value +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 GIT binary patch literal 54708 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

_x$+l{{cH8$W#CT literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..fe1fb98 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Aug 17 15:23:11 MSK 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + 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/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/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/idea-plugin/build.gradle b/idea-plugin/build.gradle new file mode 100644 index 0000000..0cd64aa --- /dev/null +++ b/idea-plugin/build.gradle @@ -0,0 +1,20 @@ +plugins { + id "org.jetbrains.intellij" version "0.2.16" +} + +apply plugin: 'org.jetbrains.intellij' + +intellij { + version '2017.2.2' //IntelliJ IDEA 2017.2.2 dependency; for a full list of IntelliJ IDEA releases please see https://www.jetbrains.com/intellij-repository/releases + pluginName 'Maven usages finder' + updateSinceUntilBuild false +} + +dependencies { + compile project(':api') +} + +jar { + // Create Uber JAR + from configurations.compile.collect { zipTree(it)} +} \ No newline at end of file diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanel.form b/idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanel.form new file mode 100644 index 0000000..b32a00d --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanel.form @@ -0,0 +1,167 @@ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanelHolder.java b/idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanelHolder.java new file mode 100644 index 0000000..782b284 --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/FindUsagesRequestConfigurationPanelHolder.java @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea; + +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; +import com.intellij.uiDesigner.core.Spacer; + +import javax.swing.*; +import java.awt.*; + +public class FindUsagesRequestConfigurationPanelHolder { + JLabel jMemberDesc; + JCheckBox jFindClasses; + JCheckBox jFindMethodOverridesUsages; + JCheckBox jFindMethods; + JCheckBox jFindFields; + JCheckBox jFindDerivedClassesUsages; + JTextField jArtifactMask; + JTextField jNumberOfLastVersions; + JCheckBox jNewTab; + JPanel contentPanel; + + /** + * @noinspection ALL + */ + private Font $$$getFont$$$(String fontName, int style, int size, Font currentFont) { + if (currentFont == null) { + return null; + } + String resultName; + if (fontName == null) { + resultName = currentFont.getName(); + } else { + Font testFont = new Font(fontName, Font.PLAIN, 10); + if (testFont.canDisplay('a') && testFont.canDisplay('1')) { + resultName = fontName; + } else { + resultName = currentFont.getName(); + } + } + return new Font(resultName, style >= 0 ? style : currentFont.getStyle(), + size >= 0 ? size : currentFont.getSize()); + } + + { + // GUI initializer generated by IntelliJ IDEA GUI Designer + // >>> IMPORTANT!! <<< + // DO NOT EDIT OR ADD ANY CODE HERE! + $$$setupUI$$$(); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() + { + contentPanel = new JPanel(); + contentPanel.setLayout(new GridLayoutManager(8, 5, new Insets(5, 5, 5, 5), -1, -1)); + final JLabel label1 = new JLabel(); + Font label1Font = this.$$$getFont$$$(null, Font.BOLD, -1, label1.getFont()); + if (label1Font != null) { + label1.setFont(label1Font); + } + label1.setText("Find Maven usages of"); + contentPanel.add(label1, + new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_SOUTHWEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label2 = new JLabel(); + Font label2Font = this.$$$getFont$$$(null, Font.BOLD, -1, label2.getFont()); + if (label2Font != null) { + label2.setFont(label2Font); + } + label2.setText("Search scope"); + contentPanel.add(label2, new GridConstraints(3, 0, 1, 3, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label3 = new JLabel(); + label3.setText("Artifact mask"); + contentPanel.add(label3, new GridConstraints(5, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(86, 16), null, 0, + false)); + final JSeparator separator1 = new JSeparator(); + contentPanel.add(separator1, + new GridConstraints(4, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, + false)); + final JSeparator separator2 = new JSeparator(); + contentPanel.add(separator2, + new GridConstraints(1, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, + false)); + jArtifactMask = new JTextField(); + jArtifactMask.setText(""); + jArtifactMask.setToolTipText( + "Artifact mask in the following format:\n::::\nUse \"*\" for any value"); + contentPanel.add(jArtifactMask, + new GridConstraints(5, 1, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), + null, 0, false)); + final JLabel label4 = new JLabel(); + label4.setText("Last artifact versions to analyze"); + contentPanel.add(label4, new GridConstraints(5, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(86, 16), null, 0, + false)); + jNumberOfLastVersions = new JTextField(); + jNumberOfLastVersions.setText(""); + jNumberOfLastVersions + .setToolTipText("Number of last artifact versions to be analyzed.\nPass \"0\" to analyze all versions"); + contentPanel.add(jNumberOfLastVersions, + new GridConstraints(5, 4, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(50, -1), + null, 0, false)); + final JPanel panel1 = new JPanel(); + panel1.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); + contentPanel.add(panel1, + new GridConstraints(2, 0, 1, 3, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, + false)); + jFindClasses = new JCheckBox(); + jFindClasses.setText("Find class usages"); + panel1.add(jFindClasses, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + jFindMethods = new JCheckBox(); + jFindMethods.setText("Find method usages"); + panel1.add(jFindMethods, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + jFindFields = new JCheckBox(); + jFindFields.setText("Find field usages"); + panel1.add(jFindFields, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JPanel panel2 = new JPanel(); + panel2.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); + contentPanel.add(panel2, + new GridConstraints(2, 3, 1, 2, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, + false)); + jFindDerivedClassesUsages = new JCheckBox(); + jFindDerivedClassesUsages.setText("Find derived classes usages"); + panel2.add(jFindDerivedClassesUsages, + new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + jFindMethodOverridesUsages = new JCheckBox(); + jFindMethodOverridesUsages.setText("Find derived methods usages"); + panel2.add(jFindMethodOverridesUsages, + new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + jNewTab = new JCheckBox(); + jNewTab.setText("Open in new tab"); + contentPanel.add(jNewTab, + new GridConstraints(7, 3, 1, 2, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final Spacer spacer1 = new Spacer(); + contentPanel.add(spacer1, + new GridConstraints(6, 0, 1, 5, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, 1, + GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); + jMemberDesc = new JLabel(); + Font jMemberDescFont = this.$$$getFont$$$("Menlo", -1, -1, jMemberDesc.getFont()); + if (jMemberDescFont != null) { + jMemberDesc.setFont(jMemberDescFont); + } + jMemberDesc.setText("com.devexperts.util.IndexedSet#"); + contentPanel.add(jMemberDesc, + new GridConstraints(0, 2, 1, 3, GridConstraints.ANCHOR_SOUTHWEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { return contentPanel; } +} \ No newline at end of file diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanel.form b/idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanel.form new file mode 100644 index 0000000..3dc33ae --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanel.form @@ -0,0 +1,86 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanelHolder.java b/idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanelHolder.java new file mode 100644 index 0000000..c003c9e --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/PluginConfigurationDialogPanelHolder.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea; + +import com.intellij.openapi.ui.VerticalFlowLayout; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; + +import javax.swing.*; +import java.awt.*; + +class PluginConfigurationDialogPanelHolder { + JButton addServerButton; + JPanel urlList; + JPanel contentPanel; + JButton addSourceRepoButton; + JPanel sourceRepos; + + private void createUIComponents() { + urlList = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false)); + sourceRepos = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false)); + } + + /** + * @noinspection ALL + */ + private Font $$$getFont$$$(String fontName, int style, int size, Font currentFont) { + if (currentFont == null) { + return null; + } + String resultName; + if (fontName == null) { + resultName = currentFont.getName(); + } else { + Font testFont = new Font(fontName, Font.PLAIN, 10); + if (testFont.canDisplay('a') && testFont.canDisplay('1')) { + resultName = fontName; + } else { + resultName = currentFont.getName(); + } + } + return new Font(resultName, style >= 0 ? style : currentFont.getStyle(), + size >= 0 ? size : currentFont.getSize()); + } + + { + // GUI initializer generated by IntelliJ IDEA GUI Designer + // >>> IMPORTANT!! <<< + // DO NOT EDIT OR ADD ANY CODE HERE! + $$$setupUI$$$(); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() + { + createUIComponents(); + contentPanel = new JPanel(); + contentPanel.setLayout(new GridLayoutManager(4, 3, new Insets(0, 0, 0, 0), -1, -1)); + contentPanel.setMinimumSize(new Dimension(400, 200)); + contentPanel.setPreferredSize(new Dimension(-1, -1)); + urlList.setBackground(new Color(-1250068)); + contentPanel.add(urlList, + new GridConstraints(1, 0, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, + false)); + final JLabel label1 = new JLabel(); + Font label1Font = this.$$$getFont$$$(null, -1, -1, label1.getFont()); + if (label1Font != null) { + label1.setFont(label1Font); + } + label1.setText("Usages server URLs"); + contentPanel.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + addServerButton = new JButton(); + addServerButton.setIcon(new ImageIcon(getClass().getResource("/general/add.png"))); + addServerButton.setText(""); + addServerButton.setToolTipText("Add new server"); + contentPanel.add(addServerButton, + new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_SOUTHEAST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JSeparator separator1 = new JSeparator(); + contentPanel.add(separator1, + new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label2 = new JLabel(); + label2.setText("Source repositories"); + contentPanel.add(label2, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + addSourceRepoButton = new JButton(); + addSourceRepoButton.setIcon(new ImageIcon(getClass().getResource("/general/add.png"))); + addSourceRepoButton.setText(""); + addSourceRepoButton.setToolTipText("Add new server"); + contentPanel.add(addSourceRepoButton, + new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_SOUTHEAST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JSeparator separator2 = new JSeparator(); + contentPanel.add(separator2, + new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + contentPanel.add(sourceRepos, + new GridConstraints(3, 0, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, + false)); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { return contentPanel; } +} diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanel.form b/idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanel.form new file mode 100644 index 0000000..7d7654a --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanel.form @@ -0,0 +1,38 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanelHolder.java b/idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanelHolder.java new file mode 100644 index 0000000..692793a --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/ServerAddressPanelHolder.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea; + +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; + +import javax.swing.*; +import java.awt.*; + +class ServerAddressPanelHolder { + JPanel contentPanel; + JButton removeButton; + JTextField urlField; + private JTextField idField; + + { + // GUI initializer generated by IntelliJ IDEA GUI Designer + // >>> IMPORTANT!! <<< + // DO NOT EDIT OR ADD ANY CODE HERE! + $$$setupUI$$$(); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() + { + contentPanel = new JPanel(); + contentPanel.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); + contentPanel.setBackground(new Color(-1250068)); + removeButton = new JButton(); + removeButton.setHideActionText(false); + removeButton.setIcon(new ImageIcon(getClass().getResource("/general/remove.png"))); + removeButton.setText(""); + removeButton.setToolTipText("Remove this server"); + contentPanel.add(removeButton, + new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + urlField = new JTextField(); + urlField.setToolTipText("Usages server URL, e.g. \"http://usages.my.company.org\""); + contentPanel.add(urlField, + new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), + null, 0, false)); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { return contentPanel; } +} diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanel.form b/idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanel.form new file mode 100644 index 0000000..de9d929 --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanel.form @@ -0,0 +1,64 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanelHolder.java b/idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanelHolder.java new file mode 100644 index 0000000..cddacb9 --- /dev/null +++ b/idea-plugin/src/main/java/com/devexperts/usages/idea/SourceRepositoryPanelHolder.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea; + +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; + +import javax.swing.*; +import java.awt.*; + +class SourceRepositoryPanelHolder { + JPanel contentPanel; + JButton removeButton; + JTextField urlField; + JTextField idField; + + { + // GUI initializer generated by IntelliJ IDEA GUI Designer + // >>> IMPORTANT!! <<< + // DO NOT EDIT OR ADD ANY CODE HERE! + $$$setupUI$$$(); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() + { + contentPanel = new JPanel(); + contentPanel.setLayout(new GridLayoutManager(1, 5, new Insets(0, 0, 0, 0), -1, -1)); + contentPanel.setBackground(new Color(-1250068)); + removeButton = new JButton(); + removeButton.setHideActionText(false); + removeButton.setIcon(new ImageIcon(getClass().getResource("/general/remove.png"))); + removeButton.setText(""); + removeButton.setToolTipText("Remove this server"); + contentPanel.add(removeButton, + new GridConstraints(0, 4, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + urlField = new JTextField(); + urlField.setToolTipText("Maven repository URL"); + contentPanel.add(urlField, + new GridConstraints(0, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(250, -1), + null, 0, false)); + final JLabel label1 = new JLabel(); + label1.setText("url"); + contentPanel.add(label1, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + final JLabel label2 = new JLabel(); + label2.setText("id"); + contentPanel.add(label2, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); + idField = new JTextField(); + idField.setToolTipText("Maven repository id, which is used in your Maven settings"); + contentPanel.add(idField, + new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(50, -1), null, 0, false)); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { return contentPanel; } +} diff --git a/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsages.kt b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsages.kt new file mode 100644 index 0000000..30b7a43 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsages.kt @@ -0,0 +1,104 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea + +import com.devexperts.usages.api.* +import com.intellij.notification.* +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.AppUIUtil + + +private val LOG = Logger.getInstance("com.devexperts.usages.idea.FindUsages") + +fun findUsagesAndShow(project: Project, member: Member) { + // Check that there is at least one Usages server in the plugin configuration + val pluginConfiguration = checkNotNull(project.getComponent(PluginConfigurationComponent::class.java).state) + val servers = pluginConfiguration.servers + if (servers.isEmpty()) { + val message = "Specify Maven Usages servers before using the plugin" + showBalloonNotification(project, message, MessageType.ERROR) + return + } + // Create a request processor + val requestConfiguration = requireNotNull( + project.getComponent(FindUsagesRequestConfigurationComponent::class.java)).state + var usagesViewer: UsagesViewer? = null + val requestProcessor = object : MemberUsageRequestProcessor( + serverUrls = pluginConfiguration.servers, + memberUsagesRequest = MemberUsageRequest( + member = member, + findClasses = requestConfiguration.findClassUsages, + findDerivedClassesUsages = requestConfiguration.findDerivedClassesUsages, + findFields = requestConfiguration.findFieldUsages, + findMethods = requestConfiguration.findMethodUsages, + findDerivedMethodsUsages = requestConfiguration.findMethodOverridesUsages, + searchScope = createArtifactMaskFromString(requestConfiguration.artifactMask + ":" + requestConfiguration.numberOfLastVersions) + ) + ) { + override fun onNewUsages(serverUrl: String, usages: List) { + AppUIUtil.invokeOnEdt { + if (usagesViewer == null) + usagesViewer = UsagesViewer(project, member, requestConfiguration.openInNewTab) + usagesViewer!!.addUsages(usages) + } + } + + override fun onError(serverUrl: String, message: String, throwable: Throwable?) { + val msg = "Error during find maven usages request from server $serverUrl: $message" + showBalloonNotification(project, message, MessageType.ERROR) + LOG.warn(msg, throwable) + } + + override fun onComplete() {} + } + // Do request as background task, could be cancelled by user + ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Find Maven usages...", true) { + override fun run(progressIndicator: ProgressIndicator) { + try { + requestProcessor.doRequest() + } catch (e: ProcessCanceledException) { + // Cancelled by user + } + } + + override fun onSuccess() { + if (usagesViewer == null) { + val message = "No Maven usages found for ${member.simpleName()}" + showBalloonNotification(project, message, MessageType.INFO) + } + } + + override fun onCancel() { + requestProcessor.cancel() + } + }) +} + +fun showBalloonNotification(project: Project, message: String, messageType: MessageType) { + val statusBar = WindowManager.getInstance().getStatusBar(project) + JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(message, messageType, null) + .createBalloon().showInCenterOf(statusBar.component) +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestAction.kt b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestAction.kt new file mode 100644 index 0000000..44ea057 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestAction.kt @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea + +import com.devexperts.usages.api.Member +import com.devexperts.usages.api.MemberType +import com.intellij.CommonBundle +import com.intellij.codeInsight.hint.HintManager +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil + +class FindUsagesRequestAction : FindUsagesRequestAbstractAction() { + override fun find(project: Project, member: Member) { + findUsagesAndShow(project, member) + } +} + +class FindUsagesRequestWithConfigurationAction : FindUsagesRequestAbstractAction() { + override fun find(project: Project, member: Member) { + FindUsagesRequestConfigurationDialog(project, member).show() + } +} + +private const val FIND_NO_USAGES_AT_CURSOR = "Cannot search for Maven usages.\n" + + "Position to an element to find usages for, and try again." + +abstract class FindUsagesRequestAbstractAction : AnAction() { + override fun actionPerformed(event: AnActionEvent) { + val project = checkNotNull(event.getData(PlatformDataKeys.PROJECT)) + val editor = event.getData(CommonDataKeys.EDITOR) + if (editor == null) { + Messages.showMessageDialog(project, FIND_NO_USAGES_AT_CURSOR, CommonBundle.getErrorTitle(), Messages.getErrorIcon()) + return + } + val member = createMember(event) + if (member == null) { + HintManager.getInstance().showErrorHint(editor, FIND_NO_USAGES_AT_CURSOR) + return + } + find(project, member) + } + + abstract fun find(project: Project, member: Member) + + private fun createMember(event: AnActionEvent): Member? { + val psiElement = event.getData(LangDataKeys.PSI_ELEMENT) + return when (psiElement) { + is PsiPackage -> createPackageMember(psiElement) + is PsiClass -> createClassMember(psiElement) + is PsiField -> createFieldMember(psiElement) + is PsiMethod -> createMethodMember(psiElement) + else -> null + } + } + + private fun createClassMember(psiClass: PsiClass): Member? { + val qualifiedName = psiClass.qualifiedName ?: return null + return Member(qualifiedName, emptyList(), MemberType.CLASS) + } + + private fun createPackageMember(psiPackage: PsiPackage) + = Member(psiPackage.qualifiedName, emptyList(), MemberType.PACKAGE) + + private fun createFieldMember(psiField: PsiField): Member? { + val className = getClassName(psiField) ?: return null + return Member(className + "#" + psiField.name, emptyList(), MemberType.FIELD) + } + + private fun createMethodMember(psiMethod: PsiMethod): Member? { + val className = getClassName(psiMethod) ?: return null + val qualifiedMethodName = className + "#" + psiMethod.name + val parameterTypes = psiMethod.parameterList.parameters + .map { it.type.canonicalText } + return Member(qualifiedMethodName, parameterTypes, MemberType.METHOD) + } + + private fun getClassName(element: PsiElement) + = PsiTreeUtil.getParentOfType(element, PsiClass::class.java)?.qualifiedName +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestConfiguration.kt b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestConfiguration.kt new file mode 100644 index 0000000..9cba723 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/FindUsagesRequestConfiguration.kt @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea + +import com.devexperts.usages.api.Member +import com.devexperts.usages.api.MemberType +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.StoragePathMacros +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import javax.swing.JComponent + +data class FindUsagesRequestConfiguration( + // Package members + val findClassUsages: Boolean = true, + // Class members + val findMethodUsages: Boolean = true, + val findFieldUsages: Boolean = true, + // Class hierarchy + val findDerivedClassesUsages: Boolean = false, +// val findBaseClassesUsages: Boolean = false, + // Method hierarchy + val findMethodOverridesUsages: Boolean = false, +// val findBaseMethodsUsages: Boolean = false, + // Search area restrictions + val artifactMask: String = "*:*:*", + val numberOfLastVersions: Int = 3, + // Interface + val openInNewTab: Boolean = false +) + +@State(name = "findUsagesRequestConfiguration") +@Storage(StoragePathMacros.ROOT_CONFIG) +class FindUsagesRequestConfigurationComponent : PersistentStateComponent { + private var settings: FindUsagesRequestConfiguration = FindUsagesRequestConfiguration() + + override fun getState(): FindUsagesRequestConfiguration = settings + + override fun loadState(settings: FindUsagesRequestConfiguration) { + this.settings = settings + } +} + +class FindUsagesRequestConfigurationDialog(private val project: Project, private val member: Member) : DialogWrapper(project) { + private val configComponent: FindUsagesRequestConfigurationComponent + private lateinit var dialogPanel: FindUsagesRequestConfigurationPanelHolder + + init { + configComponent = requireNotNull( + project.getComponent(FindUsagesRequestConfigurationComponent::class.java)) + title = "Find Maven usages" + init() + setOKButtonText("Find") + } + + override fun createCenterPanel(): JComponent { + dialogPanel = FindUsagesRequestConfigurationPanelHolder() + dialogPanel.jMemberDesc.text = member.toString() + disableUselessCheckBoxes() + loadConfig() + return dialogPanel.contentPanel + } + + private fun loadConfig() { + val cfg = configComponent.state + dialogPanel.jFindClasses.isSelected = cfg.findClassUsages +// dialogPanel.jFindBaseClassesUsages.isSelected = cfg.findBaseClassesUsages + dialogPanel.jFindDerivedClassesUsages.isSelected = cfg.findDerivedClassesUsages + dialogPanel.jFindFields.isSelected = cfg.findFieldUsages + dialogPanel.jFindMethods.isSelected = cfg.findMethodUsages +// dialogPanel.jFindBaseMethodsUsages.isSelected = cfg.findBaseMethodsUsages + dialogPanel.jFindMethodOverridesUsages.isSelected = cfg.findMethodOverridesUsages + dialogPanel.jArtifactMask.text = cfg.artifactMask + dialogPanel.jNumberOfLastVersions.text = cfg.numberOfLastVersions.toString() + dialogPanel.jNewTab.isSelected = cfg.openInNewTab + } + + private fun storeConfig() { + // TODO check artifactMask and numberOfLastVersions + configComponent.loadState(FindUsagesRequestConfiguration( + findClassUsages = dialogPanel.jFindClasses.isSelected, +// findBaseClassesUsages = dialogPanel.jFindBaseClassesUsages.isSelected, + findDerivedClassesUsages = dialogPanel.jFindDerivedClassesUsages.isSelected, + findFieldUsages = dialogPanel.jFindFields.isSelected, + findMethodUsages = dialogPanel.jFindMethods.isSelected, +// findBaseMethodsUsages = dialogPanel.jFindBaseMethodsUsages.isSelected, + findMethodOverridesUsages = dialogPanel.jFindMethodOverridesUsages.isSelected, + artifactMask = dialogPanel.jArtifactMask.text, + numberOfLastVersions = dialogPanel.jNumberOfLastVersions.text.toInt(), + openInNewTab = dialogPanel.jNewTab.isSelected + )) + } + + private fun disableUselessCheckBoxes() { + if (member.type == MemberType.PACKAGE) + return + dialogPanel.jFindClasses.isEnabled = false + if (member.type == MemberType.CLASS) + return + dialogPanel.jFindMethods.isEnabled = false + dialogPanel.jFindFields.isEnabled = false +// dialogPanel.jFindBaseClassesUsages.isEnabled = false + dialogPanel.jFindDerivedClassesUsages.isEnabled = false + if (member.type == MemberType.FIELD) { +// dialogPanel.jFindBaseMethodsUsages.isEnabled = false + dialogPanel.jFindMethodOverridesUsages.isEnabled = false + } + } + + override fun doOKAction() { + // todo ok action + storeConfig() + findUsagesAndShow(project, member) + super.doOKAction() + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/PluginConfiguration.kt b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/PluginConfiguration.kt new file mode 100644 index 0000000..95e2c9f --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/PluginConfiguration.kt @@ -0,0 +1,151 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.StoragePathMacros +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import javax.swing.JComponent + +data class PluginConfiguration( + // List of server component addresses + val servers: List, + // List of server component addresses + val sourceRepos: List +) + +data class SourceRepository(val id: String, val url: String) + +private val URL_PREFIX = "http://" + +@State(name = "findUsagesPluginConfiguration") +@Storage(StoragePathMacros.ROOT_CONFIG) +class PluginConfigurationComponent : PersistentStateComponent { + private var settings: PluginConfiguration = PluginConfiguration(servers = emptyList(), sourceRepos = emptyList()) + + override fun getState(): PluginConfiguration = settings + + override fun loadState(settings: PluginConfiguration) { + this.settings = settings + } +} + +class OpenPluginConfigurationAction : AnAction() { + override fun actionPerformed(event: AnActionEvent) { + val project = checkNotNull(event.getData(PlatformDataKeys.PROJECT)) + PluginConfigurationDialog(project).show() + } +} + +private class PluginConfigurationDialog(project: Project) : DialogWrapper(project) { + private val configComponent: PluginConfigurationComponent + private lateinit var dialogPanel: PluginConfigurationDialogPanelHolder + private lateinit var serverAddressPanels: MutableList + private lateinit var sourceRepoPanels: MutableList + + init { + configComponent = requireNotNull( + project.getComponent(PluginConfigurationComponent::class.java)) + title = "Configure Usages plugin" + init() + } + + override fun doOKAction() { + // Store plugin configuration + // todo validate + val urls = serverAddressPanels + .map { it.urlField.text } + .filter { !it.isEmpty() } + .map { canonicalAddressView(it) } + val sourceRepos = sourceRepoPanels + .filter { !it.urlField.text.isEmpty() } + .map { SourceRepository(id = it.idField.text, url = it.urlField.text) } + configComponent.loadState(PluginConfiguration(servers = urls, sourceRepos = sourceRepos)) + super.doOKAction() + } + + override fun createCenterPanel(): JComponent { + dialogPanel = PluginConfigurationDialogPanelHolder() + // Set server URLs from config + val urls = configComponent.state.servers.toMutableList() + if (urls.isEmpty()) // Should be at least one URL (may be empty) + urls.add(URL_PREFIX) + // Fill panel with server addresses + serverAddressPanels = arrayListOf() + urls.forEach { addUrlItem(it) } + // Set add new server action + dialogPanel.addServerButton.addActionListener { addUrlItem() } + // Set source repositories from config + val sourceRepos = configComponent.state.sourceRepos.toMutableList() + // Fill panel with source repositories + sourceRepoPanels = arrayListOf() + sourceRepos.forEach { addSourceRepoItem(id = it.id, url = it.url) } + // Set new source repository action + dialogPanel.addSourceRepoButton.addActionListener { addSourceRepoItem() } + // Return root panel + return dialogPanel.contentPanel + } + + private fun updateUI() { + dialogPanel.contentPanel.revalidate() + dialogPanel.contentPanel.repaint() + } + + private fun addUrlItem(url: String = URL_PREFIX) { + val serverAddressPanel = ServerAddressPanelHolder() + serverAddressPanel.urlField.text = url + serverAddressPanel.removeButton.addActionListener { + serverAddressPanels.remove(serverAddressPanel) + dialogPanel.urlList.remove(serverAddressPanel.contentPanel) + updateUI() + } + serverAddressPanels.add(serverAddressPanel) + dialogPanel.urlList.add(serverAddressPanel.contentPanel) + updateUI() + } + + private fun addSourceRepoItem(id: String = "", url: String = URL_PREFIX) { + val sourceRepoPanel = SourceRepositoryPanelHolder() + sourceRepoPanel.idField.text = id + sourceRepoPanel.urlField.text = url + sourceRepoPanel.removeButton.addActionListener { + sourceRepoPanels.remove(sourceRepoPanel) + dialogPanel.sourceRepos.remove(sourceRepoPanel.contentPanel) + updateUI() + } + sourceRepoPanels.add(sourceRepoPanel) + dialogPanel.sourceRepos.add(sourceRepoPanel.contentPanel) + updateUI() + } + + // Lead server address to canonical view + private fun canonicalAddressView(address: String): String { + var canonicalAddress = address + if (!canonicalAddress.contains("://")) + canonicalAddress = "http://" + address + if (canonicalAddress.endsWith('/')) + canonicalAddress = canonicalAddress.substring(0, canonicalAddress.length - 1) + return canonicalAddress + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesTree.kt b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesTree.kt new file mode 100644 index 0000000..f84f9a0 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesTree.kt @@ -0,0 +1,286 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea + +import com.devexperts.usages.api.MemberType +import com.devexperts.usages.api.MemberType.FIELD +import com.devexperts.usages.api.MemberType.METHOD +import com.devexperts.usages.api.MemberUsage +import com.intellij.ui.ColoredTreeCellRenderer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.TreeSpeedSearch +import com.intellij.ui.speedSearch.SpeedSearchUtil +import com.intellij.ui.treeStructure.SimpleTree +import com.intellij.util.PlatformIcons +import com.intellij.util.ui.tree.TreeUtil +import javax.swing.JTree +import javax.swing.event.TreeExpansionEvent +import javax.swing.event.TreeExpansionListener +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel +import javax.swing.tree.TreePath + +class UsagesTree(model: UsagesTreeModel) : SimpleTree(model) { + init { + // On expand action if node has one child, expand it too (recursively) + addTreeExpansionListener(object : TreeExpansionListener { + override fun treeExpanded(event: TreeExpansionEvent) { + var node = (event.path.lastPathComponent ?: return) as DefaultMutableTreeNode + while (node.childCount == 1) + node = node.getChildAt(0) as DefaultMutableTreeNode + expandPath(TreePath(node)) + } + + override fun treeCollapsed(event: TreeExpansionEvent?) {} + }) + // Set custom cell renderer + setCellRenderer(UsagesTreeCellRenderer()) + // Install additional actions + TreeUtil.installActions(this) + TreeSpeedSearch(this) // todo does not highlight text, fix it + // Do not show [SyntheticRootNode] + isRootVisible = false + } + + /** + * Expands the beginning of the tree to show the first usage + */ + fun expandFirstUsage(): TreePath { + var node = model.root as DefaultMutableTreeNode + while (!node.isLeaf) { + node = node.firstChild as DefaultMutableTreeNode + expandPath(TreePath(node.path)) + } + return TreePath(node) + } +} + +/** + * Model for work with [UsagesTree] with the specified rendering [strategy] + */ +class UsagesTreeModel(val strategy: GroupingStrategy) : DefaultTreeModel(SyntheticRootNode()) { + val rootNode = getRoot() as Node + + fun addUsages(usages: List) { + usages.forEach { usage -> + var curNode = rootNode + strategy.groupingOrder.forEach { nodeType -> + val key = nodeType.key(usage) + if (key != null) { + val rank = strategy.getRank(nodeType) + curNode = curNode.getOrCreate(rank, key, nodeType, usage, this) + curNode.usageCount++ + } + } + } + reload() + } + + /** + * Notify that [child] node has been inserted into [parent] one by the specified [index] + */ + fun fireNodeInserted(parent: Node, child: Node, index: Int) = fireTreeNodesInserted( + this, parent.path, intArrayOf(index), arrayOf(child)) +} + +/** + * Describes node types in [UsagesTree] and its rendering characteristics + */ +enum class NodeType( + /** + * Returns key for ordering nodes with the same rank. + * Returns `false` if this [NodeType] does not support the specified usage. + */ + val key: (MemberUsage) -> String?, + /** + * Renders the specified node + */ + val renderFunc: (node: UsageGroupNode, renderer: UsagesTreeCellRenderer, usage: MemberUsage) -> Unit +) { + ROOT(key = { "" }, renderFunc = { node, renderer, _ -> + renderer.append("Found usages", com.intellij.ui.SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES) + renderer.appendUsagesCount(node = node, bold = true) + }), + + + // SEARCHED_FILE, todo add it + SEARCHED_PACKAGE(key = { it.member.packageName() }, renderFunc = { node, renderer, usage -> + renderer.icon = PlatformIcons.PACKAGE_ICON + renderer.append(usage.member.packageName()) + renderer.appendUsagesCount(node = node, bold = false) + }), + SEARCHED_CLASS(key = { classKey(it) }, renderFunc = { node, renderer, usage -> + renderer.icon = PlatformIcons.CLASS_ICON + renderer.append(usage.member.simpleClassName()) + renderer.appendUsagesCount(node = node, bold = false) + }), + SEARCHED_CLASS_MEMBER(key = { classMemberKey(it) }, renderFunc = { node, renderer, usage -> + renderer.icon = when (usage.member.type) { + METHOD -> PlatformIcons.METHOD_ICON + FIELD -> PlatformIcons.FIELD_ICON + else -> throw IllegalStateException() + } + renderer.append(usage.member.simpleMemberName()) + renderer.appendUsagesCount(node = node, bold = false) + }), + + + ARTIFACT(key = { it.location.artifact.toString() }, renderFunc = { node, renderer, usage -> + renderer.icon = PlatformIcons.LIBRARY_ICON + renderer.append(usage.location.artifact.toString()) + renderer.appendUsagesCount(node = node, bold = false) + }), + USAGE_KIND(key = { it.usageKind.ordinal.toString() }, renderFunc = { node, renderer, usage -> + renderer.append(usage.usageKind.description) + renderer.appendUsagesCount(node = node, bold = false) + }), + + + TARGET_FILE(key = { it.location.file }, renderFunc = { node, renderer, usage -> + renderer.icon = PlatformIcons.FILE_ICON + renderer.append(usage.location.file.toString()) + renderer.appendUsagesCount(node = node, bold = false) + }), + TARGET_PACKAGE(key = { it.location.member.packageName() }, renderFunc = { node, renderer, usage -> + renderer.icon = PlatformIcons.PACKAGE_ICON + renderer.append(usage.location.member.packageName()) + renderer.appendUsagesCount(node = node, bold = false) + }), + TARGET_CLASS(key = { classKey(it) }, renderFunc = { node, renderer, usage -> + renderer.icon = PlatformIcons.CLASS_ICON + renderer.append(usage.member.simpleClassName()) + renderer.appendUsagesCount(node = node, bold = false) + }), + TARGET_CLASS_MEMBER(key = { classMemberKey(it) }, renderFunc = { node, renderer, usage -> + renderer.icon = when (usage.member.type) { + METHOD -> PlatformIcons.METHOD_ICON + FIELD -> PlatformIcons.FIELD_ICON + else -> throw IllegalStateException() + } + renderer.append(usage.member.simpleMemberName()) + renderer.appendUsagesCount(node = node, bold = false) + }), + + + TARGET_LINE(key = { "${it.location.file} ${it.location.lineNumber}" }, renderFunc = { node, renderer, usage -> + renderer.append(usage.location.lineNumber.toString(), com.intellij.ui.SimpleTextAttributes.GRAY_ATTRIBUTES) + renderer.append(" ") + renderer.append(usage.location.file.toString()) + renderer.appendUsagesCount(node = node, bold = false) + }) // should be leaf always +} + +/** + * Creates key for classes. + * Returns `null` if the searched element is package. + */ +private fun classKey(usage: MemberUsage): String? { + return if (usage.member.type == MemberType.PACKAGE) null else usage.member.className() +} + +/** + * Creates key for class member (field or method). + * Returns `null` if the searched element is not one of them. + */ +private fun classMemberKey(usage: MemberUsage) = when (usage.member.type) { + FIELD, METHOD -> usage.member.simpleName() + else -> null +} + +abstract class Node : DefaultMutableTreeNode() { + var usageCount: Int = 0 + private var textForSearch: String? = null + + fun getOrCreate(rank: Int, key: String, type: NodeType, usage: MemberUsage, treeModel: UsagesTreeModel): UsageGroupNode { + // Find index with first child (child_rank, child_key) >= (rank, key) + // ATTENTION! BINARY SEARCH HERE! + var l = -1 + var r = childCount + while (l + 1 < r) { + val m = (l + r) / 2 + val mChild = getChildAt(m) as UsageGroupNode + val less = mChild.rank < rank || (mChild.rank == rank && mChild.key < key) + if (less) l = m else r = m + } + // children[r] >= searched + // If found child has searched rank and key, return it + print("${children().toList().map { it as UsageGroupNode }.map { "(${it.key} ${it.rank})" }} $r ") + val insertIndex = r + if (insertIndex < childCount) { + val child = getChildAt(insertIndex) as UsageGroupNode + if (child.rank == rank && child.key == key) { + println(" FOUND") + return child + } + } + // Create new node and insert + val newNode = UsageGroupNode(type, rank, key, usage) + insert(newNode, insertIndex) + treeModel.fireNodeInserted(this, newNode, insertIndex) + println(children().toList().map { it as UsageGroupNode }.map { "(${it.key} ${it.rank})" }) + return newNode + } + + fun render(renderer: UsagesTreeCellRenderer) { + renderImpl(renderer) + textForSearch = renderer.toString() + } + + abstract fun renderImpl(renderer: UsagesTreeCellRenderer) + + override fun getUserObject() = this + + override fun toString(): String { + return textForSearch ?: super.toString() + } +} + +class UsageGroupNode(val type: NodeType, val rank: Int, val key: String, val representativeUsage: MemberUsage) : Node() { + override fun renderImpl(renderer: UsagesTreeCellRenderer) = type.renderFunc(this, renderer, representativeUsage) + + override fun toString(): String { + return type.key(representativeUsage)!! + } +} + +class SyntheticRootNode : Node() { + override fun renderImpl(renderer: UsagesTreeCellRenderer) { + // This node is synthetic and should not be shown, do nothing + } +} + +/** + * Renders [UsagesTree] nodes, delegates rendering to [UsageGroupNode]s + */ +class UsagesTreeCellRenderer : ColoredTreeCellRenderer() { + override fun customizeCellRenderer(tree: JTree, node: Any, selected: Boolean, expanded: Boolean, + leaf: Boolean, row: Int, hasFocus: Boolean) { + if (node is UsageGroupNode) + node.render(this) + else + append(node.toString()) + SpeedSearchUtil.applySpeedSearchHighlighting(tree, this, true, mySelected) + } + + fun appendUsagesCount(node: UsageGroupNode, bold: Boolean) { + val msg = " ${node.usageCount} ${(if (node.usageCount == 1) "usage" else "usages")}" + val attributes = if (bold) SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES + else SimpleTextAttributes.GRAYED_ATTRIBUTES + this.append(msg, attributes) + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesViewer.kt b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesViewer.kt new file mode 100644 index 0000000..801d26a --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/devexperts/usages/idea/UsagesViewer.kt @@ -0,0 +1,138 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.idea + +import com.devexperts.usages.api.Member +import com.devexperts.usages.api.MemberUsage +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.SimpleToolWindowPanel +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowAnchor +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.content.Content +import com.intellij.ui.content.ContentFactory +import com.intellij.util.ui.tree.TreeUtil +import javax.swing.Icon +import javax.swing.JComponent +import javax.swing.JPanel + +val USAGES_TOOL_WINDOW_TITLE = "Maven Usages" +private val FIND_USAGES_ICON = AllIcons.Actions.Find // todo use custom icon: IconLoader.getIcon("/find_usages.png") + +/** + * Shows usages in the project toolbar like standard "Find Usages" action + */ +class UsagesViewer(val project: Project, val member: Member, val newTab: Boolean) { + private val usagesModel: UsagesTreeModel + private val usagesTree: UsagesTree + private val toolWindow: ToolWindow + private val content: Content + + init { + toolWindow = getOrInitToolWindow() + val toolWindowPanel = SimpleToolWindowPanel(false) + // Add usages tree + val groupingStrategy = GroupingStrategy(arrayListOf(NodeType.ROOT, + NodeType.SEARCHED_PACKAGE, NodeType.SEARCHED_CLASS, NodeType.SEARCHED_CLASS_MEMBER, + NodeType.ARTIFACT, NodeType.USAGE_KIND, + NodeType.TARGET_PACKAGE, NodeType.TARGET_CLASS, NodeType.TARGET_CLASS_MEMBER, + NodeType.TARGET_LINE)) + usagesModel = UsagesTreeModel(groupingStrategy) + usagesTree = UsagesTree(usagesModel) + toolWindowPanel.setContent(JBScrollPane(usagesTree)) + // Add toolbar + toolWindowPanel.setToolbar(createToolbar(toolWindowPanel)) + // Create content and add it to the window + val contentTitle = "of ${member.simpleName()}" // "Maven Usages " is presented as toolWindow title + content = ContentFactory.SERVICE.getInstance().createContent(toolWindowPanel, contentTitle, true) + toolWindow.contentManager.addContent(content) + // Show the content + // todo process newTab parameter + toolWindow.contentManager.setSelectedContent(content) + if (!toolWindow.isActive) toolWindow.activate {} + toolWindow.show {} + } + + fun addUsages(newUsages: List) { + val firstAdd = usagesModel.rootNode.usageCount == 0 + usagesModel.addUsages(newUsages) + if (firstAdd) { + println("FIRST!!!!") + val firstUsagePath = usagesTree.expandFirstUsage() + usagesTree.fireTreeWillExpand(firstUsagePath) + } + } + + private fun getOrInitToolWindow(): ToolWindow { + var toolWindow = ToolWindowManager.getInstance(project).getToolWindow(USAGES_TOOL_WINDOW_TITLE) + if (toolWindow == null) { + toolWindow = ToolWindowManager.getInstance(project).registerToolWindow( + USAGES_TOOL_WINDOW_TITLE, true, ToolWindowAnchor.BOTTOM) + toolWindow.icon = FIND_USAGES_ICON + } + return toolWindow + } + + private fun createToolbar(toolWindowPanel: JPanel): JComponent { + val group = DefaultActionGroup() + group.add(usagesToolbarAction(icon = AllIcons.General.Settings, title = "Settings", + toolWindowPanel = toolWindowPanel, shortcut = "ctrl alt shift F9") { + FindUsagesRequestConfigurationDialog(project, member).show() + }) + group.add(usagesToolbarAction(icon = AllIcons.Actions.Rerun, title = "Rerun", + toolWindowPanel = toolWindowPanel, shortcut = null) { + // todo + }) + group.add(usagesToolbarAction(icon = AllIcons.Actions.Cancel, title = "Close", + toolWindowPanel = toolWindowPanel, shortcut = "ctrl shift F4") { + toolWindow.contentManager.removeContent(content, false) + }) + group.add(usagesToolbarAction(icon = AllIcons.Actions.Suspend, title = "Stop", + toolWindowPanel = toolWindowPanel, shortcut = null) { + }) + group.add(usagesToolbarAction(icon = AllIcons.Actions.Expandall, title = "Expand All", + toolWindowPanel = toolWindowPanel, shortcut = "ctrl UP") { + TreeUtil.expandAll(usagesTree) + }) + group.add(usagesToolbarAction(icon = AllIcons.Actions.Collapseall, title = "Collapse All", + toolWindowPanel = toolWindowPanel, shortcut = "ctrl DOWN") { + TreeUtil.collapseAll(usagesTree, 3) + }) + return ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, false).component + } + + private inline fun usagesToolbarAction(icon: Icon, title: String, toolWindowPanel: JPanel, shortcut: String?, + crossinline action: (AnActionEvent) -> Unit): AnAction { + return object : AnAction(title, null, icon) { + init { + if (shortcut != null) + registerCustomShortcutSet(CustomShortcutSet.fromString(shortcut), toolWindowPanel) + } + + override fun actionPerformed(e: AnActionEvent) = action(e) + } + } +} + + +class GroupingStrategy(val groupingOrder: List) { + fun getRank(nodeType: NodeType): Int = groupingOrder.indexOf(nodeType) +} \ No newline at end of file diff --git a/idea-plugin/src/main/resources/META-INF/plugin.xml b/idea-plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..302f0ae --- /dev/null +++ b/idea-plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,61 @@ + + + com.devexperts.usages.idea-plugin + ${version} + + + Maven usages finder + Provides an ability to find code usages in Maven repositories + Devexperts, LLC + + + + + + com.devexperts.usages.idea.FindUsagesRequestConfigurationComponent + + + + com.devexperts.usages.idea.PluginConfigurationComponent + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/idea-plugin/src/main/resources/icons/find_usages.png b/idea-plugin/src/main/resources/icons/find_usages.png new file mode 100644 index 0000000000000000000000000000000000000000..d4efe55d2b9295b763a50fe81f9a5f1d1c0cde77 GIT binary patch literal 3876 zcmcgvYiv|S6rQ=WyY24M2SvctLXD_|0w(@Jqd#o1OT*hCN%}g~a*j!Purn2jCItACK2K<6e=R{6TJDV=}6I zJpg}(okf^E#h=|jjA0fyq;!Eq2mu3b`$rui&)UvX&$d)?f%&vBO`7Ip+nAE{q97e^&b#w-?`U=)#k1K4K zMB(*9X2)i@4{nNY`&vR|des&Xce~wGU`$5INtTE>SJxPT0e%N;{I(rpch}X`1?{nu z+u70OGRC2x0z9A_MvE&+Yj?;Kc4b)pNlM%o(MOVAzPM!1{M#t)9L&JgTt4X%m!d05 z7c*3f^~$c6)_%o8rvd6II(9^Df%o+EeC8|?INXmJybLR~dw<7KP1jCK(>NQi8NKpM z$MN%rn*#v@kJ1$Q=0&2TW18kC&atx}IbU$x0LS^Z63OW3KY`n64>->aH2FRLG@l`K ze2~yFltx#FJM<$v*hPDdj}AJnd(qNJPlOm>!M~($6#HxMIm08c9ECO=&M9ODPKWhN zB()d;HZ%uj(DKLK0#x^t0BUdo!`8%@-glayLW?15=B00K#-~wu_z2#fo8dOz(WS;s z+>9JP9)?pV%_BjU)O1Dq4|fXR6_?v+%?kY`Q-#O_z#_D~=*YBkkbA)Ekv!e`F$q}U zGpj!?6I{*3Q9YT?g6=SLPwiGNJYtFs!(=^NvJREZIu#Z})6`%UG=}k@+e$1NwQn2- zv$ltZi4w31j&nHyz&Wscv|afp^e9&=vc|f(*k$EsZ7PNJAQrt762I-t=hkoBAr#*x zdjF|bBEY1e)xq|yqb`kYB5)Fn#xM0V@Vj0&SrLxUQOems0)D3BS!-u0EW|Xtk`hK{ z)FJ}(d$dn!sqNrS-k-pF$RY`7C4seGDDK9N2)xbv+=lty);H!H!?lV}V;08E)tbga z^5=c(8Ldzh!2x` zsjD~GU|j7r2WMp*LWD60)@T@V`L@crt+Q%2HDV2?p%70groFJaV)nr}D4YNYD2~n& zQG!VHS&b|4fMiNQN}nAqo$P|1nw5SJ#`GnMlIOqp{W4g*cuf-OA{#QVRP2m0tz(2? z3@HkHD=VPk%^ruZ5e?hhsk`Jkx~T*cbkEb?Vbj&GmqcgCH*CmAl!cWLk~nL~aAyqc z4Z*)AfaFeUit+LA0cR(?hM*$?lJo(P1n8g;#`zsfmgqv5_ra#`wUX@jUbjIP$Uu}Z z0en`!hR^CcRJfw&^EwLYr*JBccpqhfg;1AAl=9GXsPNBO7K+b6LRV33zkN}@Ant9~ r?{Bl11Z-g2;Q!!Mg!Q=pmpZ}b-L. + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.internal.MemberInternal; +import com.devexperts.usages.analyzer.walker.walkers.ZipRecursiveWalker; +import com.devexperts.usages.api.Artifact; +import com.devexperts.usages.api.Location; +import com.devexperts.usages.api.Member; +import com.devexperts.usages.api.MemberUsage; +import com.devexperts.usages.api.UsageKind; +import com.devexperts.usages.server.config.Configuration; +import com.devexperts.usages.server.indexer.MavenIndexer; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class Analyzer0 { + private static final Logger logger = LogManager.getLogger(Analyzer0.class); + + public List analyze(MavenIndexer indexer, Artifact artifact) { + List result = new ArrayList<>(); + + UsagesScanResult usagesScanResult = null; + + File artifactUsagesCacheFile = new File(getUsagesCacheFilePath(artifact)); + if (!artifactUsagesCacheFile.exists()) { + File artifactFile = indexer.downloadArtifact(artifact); + if (artifactFile == null) { + logger.error("Artifact hasn't been downloaded"); + return Collections.emptyList(); + } + try { + usagesScanResult = analyzeFile(artifactFile); + artifactUsagesCacheFile.getParentFile().mkdirs(); + usagesScanResult.writeReportAtomically(artifactUsagesCacheFile); + boolean deleted = artifactFile.delete(); + if (!deleted) { + logger.warn(artifactFile + " has not been deleted"); + } + } catch (IOException e) { + logger.error("Error while analyzing " + artifact, e); + } + } else { + try { + usagesScanResult = analyzeFile(artifactUsagesCacheFile); + } catch (IOException e) { + logger.error("Error while parsing " + artifactUsagesCacheFile, e); + } + } + + if (usagesScanResult != null) + return getMemberUsages(usagesScanResult, artifact); + + return Collections.emptyList(); + } + + static UsagesScanResult analyzeFile(File file) throws IOException { + return new UsagesScanner(ZipRecursiveWalker.ofFile(file)).analyze(); + } + + static List getMemberUsages(UsagesScanResult usagesScanResult, Artifact artifact) { + List result = new ArrayList<>(); + + Set> allClassUsages = + usagesScanResult.getUsages().getUsages().allClassUsages(); + for (Map.Entry entry : allClassUsages) { + String className = entry.getKey(); + Map>> classUsages = entry.getValue().getUsages(); + classUsages.forEach((memberName, memberEnumSetMap) -> { + MemberInternal mmm = new MemberInternal(className, memberName); + Member member = mmm.toMember(); + memberEnumSetMap.forEach((m, usages) -> { + for (Usage usage : usages) { + Location location = new Location(artifact, m.toMember(), usage.getFileName(), usage.getLineNumber()); + MemberUsage memberUsage = new MemberUsage(member, convertUseKind(usage.getUseKind()), location); + result.add(memberUsage); + } + }); + }); + } + + return result; + } + +// static Member getMember(String className, String memberName) { +// String qualifiedMemberName = className + "#" + memberName; +// if (CLASS_MEMBER_NAME.equals(memberName)) { +// return Member.createClassMember(className); +// } else if (memberName.contains("(")) { +// int indexOfLP = qualifiedMemberName.indexOf('('); +// int indexOfRP = qualifiedMemberName.indexOf(')'); +// String[] parameterTypes = new String[0]; +// if (indexOfLP + 1 != indexOfRP) { +// parameterTypes = qualifiedMemberName.substring(indexOfLP + 1, indexOfRP).split(","); +// } +// qualifiedMemberName = qualifiedMemberName.substring(0, indexOfLP); +// if (qualifiedMemberName.endsWith("")) { +// String shortClassName = className.substring(className.lastIndexOf('.') + 1); +// qualifiedMemberName = qualifiedMemberName.replace("", shortClassName); +// } +// return Member.createMethodMember(qualifiedMemberName, parameterTypes); +// } else { +// return Member.createFieldMember(qualifiedMemberName); +// } +// } + + private static UsageKind convertUseKind(UseKind useKind) { + switch (useKind) { + case ANEWARRAY: + return UsageKind.ANEWARRAY; + case ANNOTATION: + return UsageKind.ANNOTATION; + case CATCH: + return UsageKind.CATCH; + case CHECKCAST: + return UsageKind.CAST; + case CONSTANT: + return UsageKind.CONSTANT; + case EXTEND: + case IMPLEMENT: + return UsageKind.EXTEND_OR_IMPLEMENT; + case FIELD: + return UsageKind.FIELD; + case GETFIELD: + return UsageKind.GETFIELD; + case GETSTATIC: + return UsageKind.GETSTATIC; + case INSTANCEOF: + return UsageKind.CAST; + case INVOKEDYNAMIC: + return UsageKind.INVOKEDYNAMIC; + case INVOKEINTERFACE: + return UsageKind.INVOKEINTERFACE; + case INVOKESPECIAL: + return UsageKind.INVOKESPECIAL; + case INVOKESTATIC: + return UsageKind.INVOKESTATIC; + case INVOKEVIRTUAL: + return UsageKind.INVOKEVIRTUAL; + case NEW: + return UsageKind.NEW; + case OVERRIDE: + return UsageKind.OVERRIDE; + case PUTFIELD: + return UsageKind.PUTFIELD; + case PUTSTATIC: + return UsageKind.PUTSTATIC; + case RETURN: + return UsageKind.METHOD_RETURN; + case ARGUMENT: + case SIGNATURE: + return UsageKind.SIGNATURE; + case THROW: + return UsageKind.THROW; + case UNKNOWN: + return UsageKind.UNCLASSIFIED; + default: + throw new IllegalArgumentException("Wrong UseKind"); + } + } + + static String getUsagesCacheFilePath(Artifact artifact) { + StringBuilder builder = new StringBuilder() + .append(getCacheDirectory()) + .append(File.separator) + .append(artifact.getGroupId().replace('.', File.separatorChar)) + .append(File.separator) + .append(artifact.getArtifactId()) + .append(File.separator) + .append(artifact.getVersion()); + if (artifact.getClassifier() != null) { + builder.append(" (") + .append(artifact.getClassifier()) + .append(")"); + } + builder.append(".zip"); + return builder.toString(); + } + + static String getCacheDirectory() { + StringBuilder builder = new StringBuilder() + .append(Configuration.INSTANCE.getWorkDir()) + .append(File.separator) + .append("cache"); + return builder.toString(); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/AnalyzerAppConfig.java b/server/src/main/java/com/devexperts/usages/analyzer/AnalyzerAppConfig.java new file mode 100644 index 0000000..6e5af5d --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/AnalyzerAppConfig.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AnalyzerAppConfig { + public static final AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(AnalyzerAppConfig.class); + + @Bean + Analyzer0 analyzer() { + return new Analyzer0(); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/ApiScanResult.java b/server/src/main/java/com/devexperts/usages/analyzer/ApiScanResult.java new file mode 100644 index 0000000..a9bff85 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/ApiScanResult.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import org.apache.log4j.Logger; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +public class ApiScanResult { + private static final Logger logger = Logger.getLogger(ApiScanResult.class); + + private final Usages usages; + private final PublicApi api; + + public ApiScanResult(Usages usages, PublicApi api) { + this.usages = usages; + this.api = api; + } + + public void writeReport(File file) throws IOException { + logger.info("Writing " + file); + PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), Fmt.CHARSET)); + try { + api.writeReportTo(out, usages); + } finally { + out.close(); + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/ApiScanner.java b/server/src/main/java/com/devexperts/usages/analyzer/ApiScanner.java new file mode 100644 index 0000000..9efedf7 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/ApiScanner.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.walker.walkers.Walker; +import com.google.common.collect.HashMultimap; +import org.apache.log4j.Logger; + +import java.io.IOException; +import java.io.InputStream; + +public class ApiScanner { + private static final Logger logger = Logger.getLogger(ApiScanner.class); + + public static final String CLASS_SUFFIX = Constants.CLASS_SUFFIX; + + protected final Cache cache = new Cache(); + protected final Config config = new Config(); + + protected final PublicApi api = new PublicApi(cache); + + private final Walker allJarWalker; + private final Walker apiJarWalker; + + public ApiScanner(Walker allJarWalker, Walker apiJarWalker) { + this.allJarWalker = allJarWalker; + this.apiJarWalker = apiJarWalker; + } + + public ApiScanResult analyze() throws IOException { + Usages usages = new UsagesScanner(allJarWalker).analyze().getUsages(); + +// logger.info("Processing api"); + + HashMultimap processors = HashMultimap.create(); + processors.put(CLASS_SUFFIX, new Api4ClassProcessor()); + apiJarWalker.walk(new MainAnalyzer(cache, config, processors)); + + logger.info("Removing uses from api classes"); + usages.removeUsesFromClasses(api.getImplClasses()); + + return new ApiScanResult(usages, api); + } + + private class Api4ClassProcessor implements Processor { + @Override + public void process(String className, InputStream in) throws IOException { + api.parseClass(className, in); + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Cache.java b/server/src/main/java/com/devexperts/usages/analyzer/Cache.java new file mode 100644 index 0000000..68882f2 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Cache.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.internal.MemberInternal; + +import java.util.HashMap; +import java.util.Map; + +public class Cache { + private Map strings = new HashMap(); + private Map members = new HashMap(); + + public String resolveString(String s) { + String result = strings.get(s); + if (result != null) + return result; + strings.put(s, s); + return s; + } + + public MemberInternal resolveMember(String className, String memberName) { + MemberInternal m = new MemberInternal(resolveString(className), resolveString(memberName)); + MemberInternal result = members.get(m); + if (result != null) + return result; + members.put(m, m); + return m; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/ClassUsages.java b/server/src/main/java/com/devexperts/usages/analyzer/ClassUsages.java new file mode 100644 index 0000000..0fc22a0 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/ClassUsages.java @@ -0,0 +1,398 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.internal.MemberInternal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; + +public class ClassUsages { + private final Cache cache; + private final String className; + + private final Map>> usages = new TreeMap<>(); + private final Set inheritableMembers = new TreeSet<>(); // public and protected instance methods only + + public ClassUsages(Cache cache, String className) { + this.cache = cache; + this.className = className; + } + + public String getClassName() { + return className; + } + + public Map>> getUsages() { + return usages; + } + + // don't write classes that has not actual (don't store for the sake of overridableMethods only) + public boolean isEmpty() { + return usages.isEmpty(); + } + + public Set getDescendantClasses() { + Set result = new HashSet<>(); + Map> typeUseMap = usages.get(MemberInternal.CLASS_MEMBER_NAME); + if (typeUseMap != null) { + for (Map.Entry> entry : typeUseMap.entrySet()) { + Set usages = entry.getValue(); + if (usages.stream() + .anyMatch(usage -> usage.getUseKind() == UseKind.EXTEND || usage.getUseKind() == UseKind.IMPLEMENT)) + { + result.add(entry.getKey().getClassName()); + } + } + } + return result; + } + + public void inheritUseTo(Collection classAncestors) { + for (Map.Entry>> entry : usages.entrySet()) { + String member = entry.getKey(); + Map> use = entry.getValue(); + for (Map.Entry> useEntry : use.entrySet()) { + MemberInternal usedFrom = useEntry.getKey(); + Set usages = useEntry.getValue(); + for (Usage usage : usages) { + if (usage.getUseKind().inheritedUse) { + for (ClassUsages ancestor : classAncestors) { + if (ancestor.inheritableMembers.contains(member) && member.endsWith(")")) { + ancestor.addMemberUsage(member, usedFrom, usage); + } + } + } + } + } + } + } + + private static String getOuterClassName(String className) { + int i = className.indexOf('$'); + if (i < 0) { + return className; + } + return className.substring(0, i); + } + + public void cleanupInnerUsages() { + String outerClassName = getOuterClassName(className); + for (Iterator>> useIt = usages.values().iterator(); useIt.hasNext(); ) { + Map> use = useIt.next(); + for (Iterator memberIt = use.keySet().iterator(); memberIt.hasNext(); ) { + MemberInternal member = memberIt.next(); + if (outerClassName.equals(getOuterClassName(member.getClassName()))) { + memberIt.remove(); + } + } + if (use.isEmpty()) { + useIt.remove(); + } + } + } + + + public void removeUsesFromClasses(Set implClasses) { + for (Iterator>> useIt = usages.values().iterator(); useIt.hasNext(); ) { + Map> use = useIt.next(); + for (Iterator memberIt = use.keySet().iterator(); memberIt.hasNext(); ) { + MemberInternal member = memberIt.next(); + if (implClasses.contains(member.getClassName())) { + memberIt.remove(); + } + } + if (use.isEmpty()) { + useIt.remove(); + } + } + } + + public boolean isMemberUsed(String memberName) { + return usages.containsKey(memberName); + } + + // public and protected instance methods only + public Set getInheritableMembers() { + return inheritableMembers; + } + + public void addInheritableMember(String member) { + inheritableMembers.add(cache.resolveString(member)); + } + + public Set getAllUsages() { + Set result = new HashSet<>(); + for (Map> use : usages.values()) { + for (Set usages : use.values()) { + result.addAll(usages); + } + } + return result; + } + + public Map> getAllUse() { + Map> result = new TreeMap<>(); + for (Map> use : usages.values()) { + for (Map.Entry> useEntry : use.entrySet()) { + MemberInternal member = useEntry.getKey(); + Set set = result.get(member); + if (set == null) { + result.put(member, set = new HashSet<>()); + } + set.addAll(useEntry.getValue()); + } + } + return result; + } + + public Set getAllMemberUsages(String member) { + Set result = new HashSet<>(); + Map> use = usages.get(member); + if (use != null) { + use.values().forEach(result::addAll); + } + return result; + } + + public Map> getMemberUse(String member) { + Map> use = usages.get(member); + if (use == null) { + usages.put(cache.resolveString(member), use = new TreeMap<>()); + } + Map> result = new TreeMap<>(); + use.forEach((m, u) -> { + EnumSet useKinds = EnumSet.noneOf(UseKind.class); + useKinds.addAll(u.stream().map(Usage::getUseKind).collect(Collectors.toSet())); + result.put(m, useKinds); + }); + return result; + } + + public Map> getMemberUsages(String member) { + Map> use = usages.get(member); + if (use == null) { + usages.put(cache.resolveString(member), use = new TreeMap<>()); + } + return use; + } + + + private EnumSet getUseKinds(Map> use, K key) { + EnumSet useKinds = use.get(key); + if (useKinds == null) { + use.put(key, useKinds = EnumSet.noneOf(UseKind.class)); + } + return useKinds; + } + + private Set getUsages(Map> use, K key) { + Set usages = use.get(key); + if (usages == null) { + use.put(key, usages = new HashSet<>()); + } + return usages; + } + + public void addTypeUsage(MemberInternal usedFrom, Usage usage) { + addMemberUsage(MemberInternal.CLASS_MEMBER_NAME, usedFrom, usage); + } + + public void addMemberUsage(String member, MemberInternal usedFrom, Usage usage) { + getUsages(getMemberUsages(member), usedFrom).add(usage); + } + + public void readFromStream(InputStream inStream) throws IOException { + BufferedReader in = new BufferedReader(new InputStreamReader(inStream, Fmt.CHARSET)); + boolean inheritableMember = false; + String useFor = null; + Map> use = null; + String className = null; + String line; + while ((line = in.readLine()) != null) { + if (line.length() == 0 || line.startsWith(Fmt.COMMENT_PREFIX)) { + continue; + } + if (line.startsWith(Fmt.MEMBER_PREFIX)) { + if (use == null || className == null) { + throw new IOException("Invalid format -- header lines expected"); + } + String rest = line.substring(Fmt.MEMBER_PREFIX.length()); + int i = rest.indexOf(Fmt.USE_KINDS_PREFIX); + if (i < 0) { + throw new IOException("Invalid format -- missing member use kinds"); + } + String memberName = rest.substring(0, i); + Set useKinds = Usage.parseUsages(rest.substring(i + Fmt.USE_KINDS_PREFIX.length())); + MemberInternal m = cache.resolveMember(className, memberName); + getUsages(use, m).addAll(useKinds); + } else if (line.startsWith(Fmt.CLASS_PREFIX)) { + if (use == null) { + throw new IOException("Invalid format -- header lines expected"); + } + inheritableMember = false; + String rest = line.substring(Fmt.CLASS_PREFIX.length()); + int i = rest.indexOf(Fmt.USE_KINDS_PREFIX); + if (i < 0) { + className = rest; + } else { + className = rest.substring(0, i); + Set useKinds = + Usage.parseUsages(rest.substring(i + Fmt.USE_KINDS_PREFIX.length())); + getUsages(use, cache.resolveMember(className, MemberInternal.CLASS_MEMBER_NAME)).addAll(useKinds); + } + } else { + if (inheritableMember) { + addInheritableMember(useFor); + } + inheritableMember = true; + useFor = line; + use = getMemberUsages(line); + } + } + if (inheritableMember) { + addInheritableMember(useFor); + } + } + + public void writeToStream(OutputStream outStream) { + PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream, Fmt.CHARSET)); + out.print(Fmt.COMMENT_PREFIX + " Kinds of uses of class " + className); + Usage.printUsages(out, getAllUsages()); + out.println(); + out.println(); + out.println(Fmt.COMMENT_PREFIX + " ---- Summary of all classes that are using class " + className); + out.println(Fmt.COMMENT_PREFIX); + for (Map.Entry> useEntry : getUsagesOfClasses().entrySet()) { + String useClassName = useEntry.getKey(); + out.print(Fmt.COMMENT_PREFIX); + out.print(' '); + out.print(useClassName); + Usage.printUsages(out, useEntry.getValue()); + out.println(); + } + out.println(); + out.println(Fmt.COMMENT_PREFIX + " ---- Detailed uses of class " + className + " in the following format:"); + out.println(Fmt.COMMENT_PREFIX + " "); + out.println( + Fmt.COMMENT_PREFIX + " " + Fmt.CLASS_PREFIX + "" + Fmt.USE_KINDS_PREFIX + ""); + out.println(Fmt.COMMENT_PREFIX + " " + Fmt.MEMBER_PREFIX + "" + Fmt.USE_KINDS_PREFIX + + ""); + out.println(); + for (Map.Entry>> entry : usages.entrySet()) { + Map> use = entry.getValue(); + if (use.isEmpty()) { + continue; + } + out.println(entry.getKey()); + printUse(out, "", use); + } + if (!inheritableMembers.isEmpty()) { + out.println(); + out.println( + Fmt.COMMENT_PREFIX + " ---- Public and protected members (potentially inheritable and overridable)"); + out.println(); + for (String member : inheritableMembers) { + out.println(member); + } + } + out.flush(); + } + + public void printUse(PrintWriter out, String prefix, Map> use) { + String className = null; + for (Map.Entry> useEntry : use.entrySet()) { + MemberInternal m = useEntry.getKey(); + if (!m.getClassName().equals(className)) { + out.print(prefix); + out.print(Fmt.CLASS_PREFIX); + out.print(className = m.getClassName()); + Set classUseKinds = use.get(new MemberInternal(className, MemberInternal.CLASS_MEMBER_NAME)); + if (classUseKinds != null && !classUseKinds.isEmpty()) { + Usage.printUsages(out, classUseKinds); + } + out.println(); + } + if (!MemberInternal.CLASS_MEMBER_NAME.equals(m.getMemberName())) { + out.print(prefix); + out.print(Fmt.MEMBER_PREFIX); + out.print(m.getMemberName()); + Usage.printUsages(out, useEntry.getValue()); + out.println(); + } + } + } + + public Map> getUsingClasses() { + Map> result = new TreeMap>(); + for (Map> use : usages.values()) { + for (Map.Entry> useEntry : use.entrySet()) { + getUseKinds(result, useEntry.getKey().getClassName()) + .addAll(useEntry.getValue().stream().map(Usage::getUseKind).collect(Collectors.toSet())); + } + } + return result; + } + + public Map> getUsagesOfClasses() { + Map> result = new TreeMap<>(); + for (Map> use : usages.values()) { + for (Map.Entry> useEntry : use.entrySet()) { + getUsages(result, useEntry.getKey().getClassName()).addAll(useEntry.getValue()); + } + } + return result; + } + + public void fetchFrom(ClassUsages other) { + if (!className.equals(other.className)) { + throw new AssertionError("Different class names"); + } + + for (Map.Entry>> entry : other.usages.entrySet()) { + Map> v = usages.get(entry.getKey()); + if (v == null) { + usages.put(entry.getKey(), entry.getValue()); + continue; + } + for (Map.Entry> entry1 : entry.getValue().entrySet()) { + Set useKinds = v.get(entry1.getKey()); + if (useKinds == null) { + v.put(entry1.getKey(), entry1.getValue()); + } else { + useKinds.addAll(entry1.getValue()); + } + } + } + } + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/ClassUsagesAnalyzer.java b/server/src/main/java/com/devexperts/usages/analyzer/ClassUsagesAnalyzer.java new file mode 100644 index 0000000..44a926f --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/ClassUsagesAnalyzer.java @@ -0,0 +1,358 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.internal.MemberInternal; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.io.File; + +// TODO line numbers for all usages +class ClassUsagesAnalyzer extends ClassVisitor { + private static final String INIT_METHOD = ""; + private static final String DEFAULT_ANNOTATION_PROPERTY = "value"; + + private final Cache cache; + private final Usages usages; + private final String className; + private Config config; + + private int lineNumber = -1; + + private static String toClassName(String internalName) { + return internalName.replace('/', '.'); + } + + public ClassUsagesAnalyzer(Usages usages, String className, Config config) { + super(Opcodes.ASM5); + this.config = config; + this.cache = usages.getCache(); + this.usages = usages; + this.className = className; + } + + private void markMemberUse(String className, String name, MemberInternal usedFrom, UseKind useKind) { + if (className == null) { + return; + } + if (!config.excludesClassName(className)) { + usages.getUsagesForClass(className) + .addMemberUsage(name, usedFrom, new Usage(useKind, this.className, lineNumber)); + } + } + + private void makeTypeUse(String className, MemberInternal usedFrom, UseKind useKind) { + if (!config.excludesClassName(className)) { + usages.getUsagesForClass(className).addTypeUsage(usedFrom, new Usage(useKind, this.className, lineNumber)); + } + } + + private void markTypeUse(Type type, MemberInternal usedFrom, UseKind useKind) { + if (type.getSort() == Type.METHOD) { + markTypeUse(type.getReturnType(), usedFrom, UseKind.RETURN); + for (Type arg : type.getArgumentTypes()) { + markTypeUse(arg, usedFrom, UseKind.ARGUMENT); + } + return; + } + while (type.getSort() == Type.ARRAY) { + type = type.getElementType(); + } + if (type.getSort() == Type.OBJECT) { + makeTypeUse(type.getClassName(), usedFrom, useKind); + } + } + + private void markHandleUse(Handle handle, MemberInternal usedFrom, UseKind useKind) { + markTypeUse(Type.getType(handle.getDesc()), usedFrom, useKind); + String className = Type.getType(handle.getOwner()).getClassName(); + switch (handle.getTag()) { + case Opcodes.H_GETFIELD: + case Opcodes.H_GETSTATIC: + case Opcodes.H_PUTFIELD: + case Opcodes.H_PUTSTATIC: + markMemberUse(className, handle.getName(), usedFrom, useKind); + break; + case Opcodes.H_INVOKEVIRTUAL: + case Opcodes.H_INVOKESTATIC: + case Opcodes.H_INVOKESPECIAL: + case Opcodes.H_NEWINVOKESPECIAL: + case Opcodes.H_INVOKEINTERFACE: + markMemberUse(className, MemberInternal.methodMemberName(handle.getName(), Type.getType(handle.getDesc())), + usedFrom, useKind); + } + } + + private void markConstant(Object cst, MemberInternal usedFrom, UseKind useKind) { + if (cst instanceof Type) { + markTypeUse((Type) cst, usedFrom, useKind); + } else if (cst instanceof Handle) { + markHandleUse((Handle) cst, usedFrom, useKind); + } + } + + private void markSignatureUse(String signature, MemberInternal usedFrom) { + if (signature != null) { + new SignatureReader(signature).accept(new SignatureAnalyzer(usedFrom)); + } + } + + @Override + public void visitSource(String source, String debug) { + usages.putFileOfClass(className, + className.substring(0, className.lastIndexOf('.')).replace(".", File.separator) + File.separator + source); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + MemberInternal usedFrom = cache.resolveMember(className, MemberInternal.CLASS_MEMBER_NAME); + makeTypeUse(toClassName(superName), usedFrom, UseKind.EXTEND); + if (interfaces != null) { + for (String intf : interfaces) { + makeTypeUse(toClassName(intf), usedFrom, UseKind.IMPLEMENT); + } + } + markSignatureUse(signature, usedFrom); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + MemberInternal usedFrom = cache.resolveMember(className, name); + markTypeUse(Type.getType(desc), usedFrom, UseKind.FIELD); + markSignatureUse(signature, usedFrom); + if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0 + && ((access & Opcodes.ACC_STATIC) == 0)) + { + usages.getUsagesForClass(className).addInheritableMember(name); + } + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + String methodMemberName = MemberInternal.methodMemberName(name, Type.getType(desc)); + MemberInternal usedFrom = cache.resolveMember(className, methodMemberName); + markTypeUse(Type.getType(desc), usedFrom, UseKind.UNKNOWN); // will be replaced by RETURN/ARGUMENT + markSignatureUse(signature, usedFrom); + if (exceptions != null) { + for (String ex : exceptions) { + makeTypeUse(toClassName(ex), usedFrom, UseKind.THROW); + } + } + if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0 + && ((access & Opcodes.ACC_STATIC) == 0) + && !name.equals(INIT_METHOD)) + { + usages.getUsagesForClass(className).addInheritableMember(methodMemberName); + } + return new MethodAnalyzer(usedFrom); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + Type annotationType = Type.getType(desc); + MemberInternal usedFrom = cache.resolveMember(className, MemberInternal.CLASS_MEMBER_NAME); + markTypeUse(annotationType, usedFrom, UseKind.ANNOTATION); + return new AnnotationAnalyzer(annotationType.getClassName(), usedFrom); + } + + private class MethodAnalyzer extends MethodVisitor { + private MemberInternal method; + + private MethodAnalyzer(MemberInternal method) { + super(Opcodes.ASM5); + this.method = method; + } + + @Override + public void visitLineNumber(int line, Label start) { + lineNumber = line; + } + + @Override + public void visitEnd() { + lineNumber = -1; + } + + @Override + public void visitTypeInsn(int opcode, String type) { + UseKind useKind; + switch (opcode) { + case Opcodes.NEW: + useKind = UseKind.NEW; + break; + case Opcodes.ANEWARRAY: + useKind = UseKind.ANEWARRAY; + break; + case Opcodes.CHECKCAST: + useKind = UseKind.CHECKCAST; + break; + case Opcodes.INSTANCEOF: + useKind = UseKind.INSTANCEOF; + break; + default: + useKind = UseKind.UNKNOWN; + } + markTypeUse(Type.getObjectType(type), method, useKind); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + UseKind useKind; + switch (opcode) { + case Opcodes.GETFIELD: + useKind = UseKind.GETFIELD; + break; + case Opcodes.PUTFIELD: + useKind = UseKind.PUTFIELD; + break; + case Opcodes.GETSTATIC: + useKind = UseKind.GETSTATIC; + break; + case Opcodes.PUTSTATIC: + useKind = UseKind.PUTSTATIC; + break; + default: + useKind = UseKind.UNKNOWN; + } + markMemberUse(toClassName(owner), name, method, useKind); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + UseKind useKind; + switch (opcode) { + case Opcodes.INVOKEVIRTUAL: + useKind = UseKind.INVOKEVIRTUAL; + break; + case Opcodes.INVOKESPECIAL: + useKind = UseKind.INVOKESPECIAL; + break; + case Opcodes.INVOKESTATIC: + useKind = UseKind.INVOKESTATIC; + break; + case Opcodes.INVOKEINTERFACE: + useKind = UseKind.INVOKEINTERFACE; + break; + default: + useKind = UseKind.UNKNOWN; + } + if (!owner.startsWith("[")) { + markMemberUse(toClassName(owner), MemberInternal.methodMemberName(name, Type.getType(desc)), method, useKind); + } + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) { + markHandleUse(bsm, method, UseKind.INVOKEDYNAMIC); + for (Object arg : bsmArgs) { + markConstant(arg, method, UseKind.INVOKEDYNAMIC); + } + } + + @Override + public void visitLdcInsn(Object cst) { + markConstant(cst, method, UseKind.CONSTANT); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { + makeTypeUse(toClassName(type), method, UseKind.CATCH); + } + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return new AnnotationAnalyzer(className, method); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + return new AnnotationAnalyzer(Type.getType(desc).getClassName(), method); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + return new AnnotationAnalyzer(Type.getType(desc).getClassName(), method); + } + } + + private class SignatureAnalyzer extends SignatureVisitor { + private final MemberInternal usedFrom; + + private SignatureAnalyzer(MemberInternal usedFrom) { + super(Opcodes.ASM5); + this.usedFrom = usedFrom; + } + + @Override + public void visitClassType(String name) { + makeTypeUse(toClassName(name), usedFrom, UseKind.SIGNATURE); + } + } + + private class AnnotationAnalyzer extends AnnotationVisitor { + private final String annotationClassName; + private final MemberInternal usedFrom; + + public AnnotationAnalyzer(String annotationClassName, MemberInternal usedFrom) { + super(Opcodes.ASM5); + this.annotationClassName = annotationClassName; + this.usedFrom = usedFrom; + } + + @Override + public void visit(String name, Object value) { + markMemberUse(annotationClassName, annotationMemberName(name), usedFrom, UseKind.ANNOTATION); + markConstant(value, usedFrom, UseKind.ANNOTATION); + } + + @Override + public void visitEnum(String name, String desc, String value) { + markMemberUse(annotationClassName, annotationMemberName(name), usedFrom, UseKind.ANNOTATION); + markMemberUse(Type.getType(desc).getClassName(), value, usedFrom, UseKind.ANNOTATION); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + markMemberUse(annotationClassName, annotationMemberName(name), usedFrom, UseKind.ANNOTATION); + markTypeUse(Type.getType(desc), usedFrom, UseKind.ANNOTATION); + return this; + } + + @Override + public AnnotationVisitor visitArray(String name) { + markMemberUse(annotationClassName, annotationMemberName(name), usedFrom, UseKind.ANNOTATION); + return this; + } + + private String annotationMemberName(String name) { + return (name == null ? DEFAULT_ANNOTATION_PROPERTY : name) + "()"; + } + } +} \ No newline at end of file diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Config.java b/server/src/main/java/com/devexperts/usages/analyzer/Config.java new file mode 100644 index 0000000..f99e48a --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Config.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Config { + public static final String USAGES_PROP = "usages"; + public static final String API_PROP = "api"; + public static final String EXCLUDES_PROP = "excludes"; + private Matcher excludesMatcher = null; + + public Config() { + } // do not create -- static only + + public static String getUsages() { + return System.getProperty(USAGES_PROP, "usages.zip"); + } + + public static String getApi() { + return System.getProperty(API_PROP, "api.txt"); + } + + public static String getExcludes() { + return System.getProperty(EXCLUDES_PROP, + "java.*,javax.*,javafx.*,sun.*,sunw.*,COM.rsa.*,com.sun.*,com.oracle.*"); + } + + public static Pattern globToPattern(String s, boolean supportComma) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '*': + sb.append(".*"); + break; + case '?': + sb.append("."); + break; + case ',': + if (supportComma) { + sb.append("|"); + break; + } + default: + sb.append(Pattern.quote(String.valueOf(c))); + break; + } + } + return Pattern.compile(sb.toString()); + } + + public boolean excludesClassName(String excludesClassName) { + if (excludesMatcher == null) + excludesMatcher = globToPattern(getExcludes(), true).matcher(excludesClassName); + else + excludesMatcher.reset(excludesClassName); + return excludesMatcher.matches(); + + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Constants.java b/server/src/main/java/com/devexperts/usages/analyzer/Constants.java new file mode 100644 index 0000000..b9849c0 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Constants.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +public class Constants { + public static final String CLASS_SUFFIX = ".class"; + public static final String USAGES_SUFFIX = ".usages"; +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/FileUtils.java b/server/src/main/java/com/devexperts/usages/analyzer/FileUtils.java new file mode 100644 index 0000000..66143a4 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/FileUtils.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import java.io.File; +import java.io.IOException; +import java.util.Random; + +/** + * Some methods of io.Files that java 6 lacks of + */ +public class FileUtils { + private static final String tempFormat = "%s.%d.tmp"; + + public static File createTempFile(String file) throws IOException { + Random rand = new Random(); + while (true) { + try { + File tempFile = new File(String.format(tempFormat, file, Math.abs(rand.nextInt()))); + if (tempFile.createNewFile()) + return tempFile; + } catch (IOException e) { + throw new IOException("Cannot create temp file for " + file); + } + } + } + + public static File createTempFile(File file) throws IOException { + return createTempFile(file.toString()); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Fmt.java b/server/src/main/java/com/devexperts/usages/analyzer/Fmt.java new file mode 100644 index 0000000..fa5eb76 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Fmt.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import java.nio.charset.Charset; + +class Fmt { + public static final Charset CHARSET = Charset.forName("UTF-8"); + + public static final String COMMENT_PREFIX = "#"; + public static final String CLASS_PREFIX = "\t"; + public static final String MEMBER_PREFIX = "\t\t"; + public static final String USE_KINDS_PREFIX = " -- "; + public static final String USE_KINDS_SEPARATOR = ","; +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/MainAnalyzer.java b/server/src/main/java/com/devexperts/usages/analyzer/MainAnalyzer.java new file mode 100644 index 0000000..25741e3 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/MainAnalyzer.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; +import com.google.common.collect.Multimap; +import org.apache.log4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; + +class MainAnalyzer implements FileAnalyzer { + private static final Logger logger = Logger.getLogger(MainAnalyzer.class); + + private final Cache cache; + private final Config config; + private final Multimap processors; + + public MainAnalyzer(Cache cache, Config config, Multimap processors) { + this.cache = cache; + this.config = config; + this.processors = processors; + } + + @Override + public void process(FileInfo fileInfo) throws IOException { + String className = pathToClassName(fileInfo.getBaseName()); + if (config.excludesClassName(className)) + return; + + Collection curProcessors = this.processors.get(fileInfo.getSuffix()); + if (!curProcessors.isEmpty()) { +// logger.info("Processing " + fileInfo.getPath()); + for (Processor processor : curProcessors) { + InputStream in = fileInfo.openInputStream(); + try { + processor.process(className, in); + } finally { + in.close(); + } + } + } + } + + private String pathToClassName(String path) { + return cache.resolveString(path.replace('/', '.')); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Processing.java b/server/src/main/java/com/devexperts/usages/analyzer/Processing.java new file mode 100644 index 0000000..380f36a --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Processing.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.walker.walkers.FilePackWalker; +import com.devexperts.usages.analyzer.walker.walkers.Walker; +import com.devexperts.usages.analyzer.walker.walkers.ZipRecursiveWalker; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class Processing { + private final List allJarFiles = new ArrayList(); + private final List apiJarFiles = new ArrayList(); + + public final String resultFileName; + + public Processing(List allJarFiles, List apiJarFiles, String resultFileName) { + this.allJarFiles.addAll(allJarFiles); + this.apiJarFiles.addAll(apiJarFiles); + this.resultFileName = resultFileName; + } + + public void go() throws IOException { + Walker allJarWalkers = filesToWalkers(allJarFiles); + + if (apiJarFiles.isEmpty()) { + new UsagesScanner(allJarWalkers).analyze() + .writeReport(new File(resultFileName)); + } else { + Walker apiJarWalkers = filesToWalkers(apiJarFiles); + + new ApiScanner(allJarWalkers, apiJarWalkers).analyze() + .writeReport(new File(Config.getApi())); + } + } + + private static Walker filesToWalkers(List files) { + return FilePackWalker.fromFilenames(files, + new SmartDirectoriesWalker.D().delegateTo( + new ZipRecursiveWalker.D().passToProcessor())); + } + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Processor.java b/server/src/main/java/com/devexperts/usages/analyzer/Processor.java new file mode 100644 index 0000000..6d6406a --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Processor.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import java.io.IOException; +import java.io.InputStream; + +public interface Processor { + void process(String className, InputStream in) throws IOException; +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/PublicApi.java b/server/src/main/java/com/devexperts/usages/analyzer/PublicApi.java new file mode 100644 index 0000000..3b7f999 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/PublicApi.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.internal.MemberInternal; +import org.apache.log4j.Logger; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; + +class PublicApi { + private static final Logger logger = Logger.getLogger(PublicApi.class); + + private final Cache cache; + + private final Set implClasses = new HashSet(); + private final Set apiMembers = new TreeSet(); + private final Set deprecatedMembers = new TreeSet(); + + public PublicApi(Cache cache) { + this.cache = cache; + } + + public Set getImplClasses() { + return implClasses; + } + + public void addImplClass(String className) { + implClasses.add(className); + } + + public void addApiMember(String className, String memberName, boolean deprecated) { + MemberInternal member = cache.resolveMember(className, memberName); + apiMembers.add(member); + if (deprecated) + deprecatedMembers.add(member); + } + + public void parseClass(String className, InputStream inStream) throws IOException { + ClassReader cr = new ClassReader(inStream); + if (!className.equals(cr.getClassName().replace('/', '.'))) + logger.warn("Unexpected class name: " + cr.getClassName() + " for class " + className); + else + cr.accept( + new PublicApiAnalyzer(this, className), + ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE); + } + + public void writeReportTo(PrintWriter out, Usages usages) { + out.println(Fmt.COMMENT_PREFIX + " ---- Unused deprecated api classes"); + out.println(); + Set unusedClasses = new HashSet(); + for (MemberInternal member : deprecatedMembers) { + if (member.getMemberName().equals(MemberInternal.CLASS_MEMBER_NAME)) { + if (!usages.isClassUsed(member.getClassName())) { + unusedClasses.add(member.getClassName()); + out.println(member.getClassName()); + } + } + } + + out.println(); + out.println(Fmt.COMMENT_PREFIX + " ---- Unused deprecated api members in the following format:"); + out.println(Fmt.COMMENT_PREFIX + " "); + out.println(Fmt.COMMENT_PREFIX + " \t"); + out.println(); + String className = null; + for (MemberInternal member : deprecatedMembers) { + if (unusedClasses.contains(member.getClassName())) + continue; + if (member.getMemberName().equals(MemberInternal.CLASS_MEMBER_NAME)) + continue; + if (!usages.isMemberUsed(member)) { + if (!member.getClassName().equals(className)) { + className = member.getClassName(); + out.println(className); + } + out.println("\t" + member.getMemberName()); + } + } + + out.println(); + out.println(Fmt.COMMENT_PREFIX + " ---- Still used deprecated api classes in the following format:"); + out.println(Fmt.COMMENT_PREFIX + " " + Fmt.USE_KINDS_PREFIX + " # summary of all uses"); + out.println(Fmt.COMMENT_PREFIX + " " + Fmt.CLASS_PREFIX + "" + Fmt.USE_KINDS_PREFIX + ""); + out.println(Fmt.COMMENT_PREFIX + " " + Fmt.MEMBER_PREFIX + "" + Fmt.USE_KINDS_PREFIX + ""); + out.println(); + for (MemberInternal member : deprecatedMembers) { + if (member.getMemberName().equals(MemberInternal.CLASS_MEMBER_NAME)) { + if (usages.isClassUsed(member.getClassName())) { + ClassUsages cu = usages.getUsagesForClass(member.getClassName()); + out.print(member.getClassName()); + Usage.printUsages(out, cu.getAllUsages()); + out.println(); + cu.printUse(out, "", cu.getAllUse()); + } + } + } + + out.println(); + out.println(Fmt.COMMENT_PREFIX + " ---- Still used deprecated api members in the following format:"); + out.println(Fmt.COMMENT_PREFIX + " "); + out.println(Fmt.COMMENT_PREFIX + " \t" + Fmt.USE_KINDS_PREFIX + " # summary of all uses"); + out.println(Fmt.COMMENT_PREFIX + " \t" + Fmt.CLASS_PREFIX + "" + Fmt.USE_KINDS_PREFIX + ""); + out.println(Fmt.COMMENT_PREFIX + " \t" + Fmt.MEMBER_PREFIX + "" + Fmt.USE_KINDS_PREFIX + ""); + out.println(); + className = null; + for (MemberInternal member : deprecatedMembers) { + if (member.getMemberName().equals(MemberInternal.CLASS_MEMBER_NAME)) + continue; + if (!usages.isMemberUsed(member)) + continue; + if (!member.getClassName().equals(className)) { + className = member.getClassName(); + out.println(className); + } + ClassUsages cu = usages.getUsagesForClass(member.getClassName()); + out.print("\t" + member.getMemberName()); + Usage.printUsages(out, cu.getAllMemberUsages(member.getMemberName())); + out.println(); + cu.printUse(out, "\t", cu.getMemberUsages(member.getMemberName())); + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/PublicApiAnalyzer.java b/server/src/main/java/com/devexperts/usages/analyzer/PublicApiAnalyzer.java new file mode 100644 index 0000000..b9812a5 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/PublicApiAnalyzer.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.internal.MemberInternal; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +class PublicApiAnalyzer extends ClassVisitor { + private final PublicApi api; + private final String className; + + public PublicApiAnalyzer(PublicApi api, String className) { + super(Opcodes.ASM4); + this.api = api; + this.className = className; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + api.addImplClass(className); + if (isPublicApi(access)) + api.addApiMember(className, MemberInternal.CLASS_MEMBER_NAME, isDeprecated(access)); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + if (isPublicApi(access)) + api.addApiMember(className, name, isDeprecated(access)); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + if (isPublicApi(access)) + api.addApiMember(className, MemberInternal.methodMemberName(name, Type.getType(desc)), isDeprecated(access)); + return null; + } + + private static boolean isPublicApi(int access) { + return (access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0; + } + + private static boolean isDeprecated(int access) { + return (access & (Opcodes.ACC_DEPRECATED)) != 0; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/SmartDirectoriesWalker.java b/server/src/main/java/com/devexperts/usages/analyzer/SmartDirectoriesWalker.java new file mode 100644 index 0000000..ccbd8fe --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/SmartDirectoriesWalker.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; +import com.devexperts.usages.analyzer.walker.info.PlainFileInfo; +import com.devexperts.usages.analyzer.walker.walkers.Delegating; +import com.devexperts.usages.analyzer.walker.walkers.DelegatingWalker; +import com.devexperts.usages.analyzer.walker.walkers.MidDelegating; +import com.devexperts.usages.analyzer.walker.walkers.Passing; +import com.devexperts.usages.analyzer.walker.walkers.TerminalWalker; +import org.apache.log4j.Logger; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.regex.Pattern; + +/** + * Walks through filesystem. + * Understands constructions like "./fil?name.txt", "./*.txt" or "./**.txt". + * + * @param type of fileInfo passed to analyser at the end of delegation chain + */ +class SmartDirectoriesWalker extends DelegatingWalker { + private static final Logger logger = Logger.getLogger(SmartDirectoriesWalker.class); + + private final File file; + + public static SmartDirectoriesWalker ofFile(File file) { + return new SmartDirectoriesWalker(file, TerminalWalker.getDelegating()); + } + + public SmartDirectoriesWalker(File rootFile, Delegating delegating) { + super(delegating); + this.file = rootFile; + } + + @Override + public void walk(FileAnalyzer analyzer) throws IOException { +// logger.info("Processing " + file); + String name = file.getName(); + if (!name.contains("*") && !name.contains("?")) { + delegating.makeDelegate(new PlainFileInfo(file)) + .walk(analyzer); + return; + } + File parentFile = file.getParentFile(); + if (name.contains("**")) { + File[] dirs = parentFile.listFiles(); + if (dirs != null) { + for (File dir : dirs) { + if (dir.isDirectory()) { + new SmartDirectoriesWalker(new File(dir, name), delegating) + .walk(analyzer); + } + } + } + name = name.replace("**", "*"); + } + final Pattern pattern = Config.globToPattern(name, false); + String[] fileNames = parentFile.list(new FilenameFilter() { + public boolean accept(File dir, String fileName) { + return pattern.matcher(fileName).matches(); + } + }); + if (fileNames != null) { + for (String fileName : fileNames) { + File f = new File(parentFile, fileName); +// logger.info("Processing " + f); + new SmartDirectoriesWalker(f, delegating) + .walk(analyzer); + } + } + } + + public static class D extends Passing { + @Override + public MidDelegating delegateTo( + final Delegating nextDelegating) { + return new MidDelegating() { + @Override + public DelegatingWalker makeDelegate(PlainFileInfo info) { + return new SmartDirectoriesWalker(info.getFile(), nextDelegating); + } + }; + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Usage.java b/server/src/main/java/com/devexperts/usages/analyzer/Usage.java new file mode 100644 index 0000000..597a1f6 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Usage.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Usage { + private UseKind useKind; + private String fileName; + private int lineNumber; + + public Usage(UseKind useKind, String fileName, int lineNumber) { + this.useKind = useKind; + this.fileName = fileName; + this.lineNumber = lineNumber; + } + + public UseKind getUseKind() { + return useKind; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public int getLineNumber() { + return lineNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Usage usage = (Usage) o; + + if (lineNumber != usage.lineNumber) { + return false; + } + if (useKind != usage.useKind) { + return false; + } + return fileName != null ? fileName.equals(usage.fileName) : usage.fileName == null; + } + + @Override + public int hashCode() { + int result = useKind != null ? useKind.hashCode() : 0; + result = 31 * result + (fileName != null ? fileName.hashCode() : 0); + result = 31 * result + lineNumber; + return result; + } + + public static void printUsages(PrintWriter out, Set useKinds) { + out.print(Fmt.USE_KINDS_PREFIX); + boolean firstKind = true; + for (Usage usage : useKinds) { + if (firstKind) + firstKind = false; + else + out.print(Fmt.USE_KINDS_SEPARATOR); + out.print(usage.getUseKind() + "(" + usage.getFileName() + ":" + usage.getLineNumber() + ")"); + } + } + + public static Set parseUsages(String s) { + Set usages = new HashSet<>(); + StringTokenizer st = new StringTokenizer(s, Fmt.USE_KINDS_SEPARATOR); + while (st.hasMoreTokens()) { + Pattern pattern = Pattern.compile("(\\w+)\\(([^():]*):(-?\\d+)\\)"); + Matcher matcher = pattern.matcher(st.nextToken()); + if (matcher.matches()) { + UseKind useKind = UseKind.valueOf(matcher.group(1)); + String fileName = matcher.group(2); + int lineNumber = Integer.parseInt(matcher.group(3)); + usages.add(new Usage(useKind, fileName, lineNumber)); + } + } + return usages; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/Usages.java b/server/src/main/java/com/devexperts/usages/analyzer/Usages.java new file mode 100644 index 0000000..d4df6a8 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/Usages.java @@ -0,0 +1,214 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.internal.MemberInternal; +import com.devexperts.usages.analyzer.tune.UsagesKeeper; +import org.apache.log4j.Logger; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +class Usages { + private static final Logger logger = Logger.getLogger(Usages.class); + + private final Cache cache; + private final UsagesKeeper usages; + private final Map> descendants = new HashMap<>(); + private final Config config; + private final Map filesOfClasses = new HashMap<>(); + private boolean needPostprocessing = true; + + public Usages(Cache cache, UsagesKeeper usages, Config config) { + this.cache = cache; + this.usages = usages; + this.config = config; + } + + public void putFileOfClass(String className, String fileName) { + filesOfClasses.put(className, fileName); + } + + public void setNeedPostprocessing(boolean needPostprocessing) { + this.needPostprocessing = needPostprocessing; + } + + public Cache getCache() { + return cache; + } + + private Set getDescendantsRec(String className) { + Set ds = descendants.get(className); + if (ds != null) { + return ds; + } + descendants.put(className, Collections.emptySet()); // avoid infinite recursion + ClassUsages classUsage = usages.get(className); + Set result = new HashSet<>(); + if (classUsage != null) { + Set immediateDescendants = classUsage.getDescendantClasses(); + result.addAll(immediateDescendants); + for (String descendant : immediateDescendants) { + result.addAll(getDescendantsRec(descendant)); + } + } + if (!result.isEmpty()) { + descendants.put(className, result); + } + return result; + } + + public void analyze() { + if (needPostprocessing) { + Set> usageEntries = usages.allClassUsages(); + + // setting information about source files + usageEntries.forEach(entry -> entry.getValue().getUsages() + .forEach((s, memberSetMap) -> memberSetMap.forEach((member, usages1) -> usages1 + .forEach(usage -> usage + .setFileName(filesOfClasses.getOrDefault(usage.getFileName(), usage.getFileName())))))); + + logger.info("Analyzing overrides"); + // build descendants map recursively with memorization + for (Map.Entry entry : usageEntries) { + String className = entry.getKey(); + ClassUsages cu = entry.getValue(); + Set classMembers = cu.getInheritableMembers(); + Set classDescendants = getDescendantsRec(className); + for (String descendantClassName : classDescendants) { + ClassUsages descendantCU = usages.get(descendantClassName); + if (descendantCU != null) { + for (String descendantMember : descendantCU.getInheritableMembers()) { + if (classMembers.contains(descendantMember) && descendantMember.endsWith(")")) { + + cu.addMemberUsage(descendantMember, + cache.resolveMember(descendantClassName, descendantMember), + new Usage(UseKind.OVERRIDE, filesOfClasses.get(className), -1)); + } + } + } + } + } + + logger.info("Analyzing inheritance"); + // build ancestors map by reversing descendants map + Map> ancestors = new HashMap<>(); + for (Map.Entry> entry : descendants.entrySet()) { + ClassUsages ancestor = usages.get(entry.getKey()); + for (String descendant : entry.getValue()) { + Set classAncestors = ancestors.get(descendant); + if (classAncestors == null) { + ancestors.put(descendant, classAncestors = new HashSet<>()); + } + classAncestors.add(ancestor); + } + } + for (Map.Entry entry : usageEntries) { + String className = entry.getKey(); + Set classAncestors = ancestors.get(className); + if (classAncestors == null) { + continue; + } + ClassUsages cu = entry.getValue(); + cu.inheritUseTo(classAncestors); + } + + logger.info("Cleaning up inner class usages"); + for (Map.Entry entry : usageEntries) { + entry.getValue().cleanupInnerUsages(); + } + } + } + + public void removeUsesFromClasses(Set implClasses) { + for (Map.Entry entry : usages.allClassUsages()) { + entry.getValue().removeUsesFromClasses(implClasses); + } + } + + public boolean isClassUsed(String className) { + ClassUsages cu = usages.get(className); + return cu != null && !cu.isEmpty(); + } + + public boolean isMemberUsed(MemberInternal member) { + ClassUsages cu = usages.get(member.getClassName()); + return cu != null && cu.isMemberUsed(member.getMemberName()); + } + + public void writeToZipFile(OutputStream outputStream) throws IOException { + ZipOutputStream zos = new ZipOutputStream(outputStream); + try { + Set> usagesToWrite = usages.allClassUsages(); + int size = usagesToWrite.size(); + int done = 0; + int lastPercent = 0; + long lastTime = System.currentTimeMillis(); + + for (Map.Entry entry : usagesToWrite) { + String name = entry.getKey(); + ClassUsages usages = entry.getValue(); + if (usages.isEmpty()) { + continue; + } + zos.putNextEntry(new ZipEntry(name.replace('.', '/') + Constants.USAGES_SUFFIX)); + usages.writeToStream(zos); + zos.closeEntry(); + done++; + int percent = 100 * done / size; + long time = System.currentTimeMillis(); + if (done == size || percent > lastPercent && time > lastTime + 1000) { + lastPercent = percent; + lastTime = time; + logger.info(percent + "% done"); + } + } + logger.info("Writing zip file directory"); + zos.finish(); + } finally { + zos.close(); + } + } + + public UsagesKeeper getUsages() { + return usages; + } + + public ClassUsages getUsagesForClass(String className) { + return usages.getOrEmpty(className); + } + + public void parseClass(String className, InputStream inStream) throws IOException { + ClassReader cr = new ClassReader(inStream); + if (!className.equals(cr.getClassName().replace('/', '.'))) { + logger.info("Unexpected class name: " + cr.getClassName() + " for class " + className); + } else { + cr.accept(new ClassUsagesAnalyzer(this, className, config), ClassReader.SKIP_FRAMES); + } + } + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/UsagesScanResult.java b/server/src/main/java/com/devexperts/usages/analyzer/UsagesScanResult.java new file mode 100644 index 0000000..ea53bae --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/UsagesScanResult.java @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.concurrent.ConcurrentOutputStream; +import com.devexperts.usages.analyzer.internal.MemberInternal; +import org.apache.log4j.Logger; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +public class UsagesScanResult { + private static final Logger logger = Logger.getLogger(UsagesScanResult.class); + + private final Usages usages; + + public UsagesScanResult(Usages usages) { + this.usages = usages; + } + + public Map> getElementUsages(String name) throws IOException { + MemberInternal member = MemberInternal.valueOf(name); + Map> answer = usages.getUsagesForClass(member.getClassName()) + .getMemberUse(member.getMemberName()); + + HashMap> clone = new HashMap>(); + for (Map.Entry> entry : answer.entrySet()) { + clone.put(entry.getKey(), EnumSet.copyOf(entry.getValue())); + } + return clone; + } + + public void writeReport(File zipFile) throws IOException { + write(zipFile, false, false); + } + + public void writeReportAtomically(File zipFile) throws IOException { + write(zipFile, true, false); + } + + public void writeReportAtomicallyForcely(File zipFile) throws IOException { + write(zipFile, true, true); + } + + private void write(File zipFile, boolean atomically, boolean force) throws IOException { + if (isEmpty()) { + logger.info("Nothing found"); + if (!force) + return; + } + logger.info("Writing " + zipFile); + OutputStream outputStream = atomically ? new ConcurrentOutputStream(zipFile) : new FileOutputStream(zipFile); + usages.writeToZipFile(outputStream); + logger.info("Completed"); + } + + private boolean isEmpty() { + for (Map.Entry entry : usages.getUsages().allClassUsages()) { + if (!entry.getValue().isEmpty()) + return false; + } + return true; + } + + Usages getUsages() { + return usages; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/UsagesScanner.java b/server/src/main/java/com/devexperts/usages/analyzer/UsagesScanner.java new file mode 100644 index 0000000..ff33230 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/UsagesScanner.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +import com.devexperts.usages.analyzer.tune.SimpleUsagesKeeper; +import com.devexperts.usages.analyzer.tune.UsagesKeeper; +import com.devexperts.usages.analyzer.walker.walkers.Walker; +import com.google.common.collect.HashMultimap; +import org.apache.log4j.Logger; + +import java.io.IOException; +import java.io.InputStream; + +public class UsagesScanner { + private static final Logger logger = Logger.getLogger(UsagesScanner.class); + + public static final String CLASS_SUFFIX = Constants.CLASS_SUFFIX; + public static final String USAGES_SUFFIX = Constants.USAGES_SUFFIX; + + protected final Cache cache = new Cache(); + protected final Config config = new Config(); + + protected final Usages usages; + + private final Walker walker; + + public UsagesScanner(Walker walker) { + this.walker = walker; + this.usages = new Usages(cache, new SimpleUsagesKeeper(cache), config); + } + + public UsagesScanner(Walker walker, UsagesKeeper usagesKeeper) { + this.walker = walker; + this.usages = new Usages(cache, usagesKeeper, config); + } + + public UsagesScanResult analyze() throws IOException { +// logger.info("Processing usages"); + + HashMultimap processors = HashMultimap.create(); + processors.put(CLASS_SUFFIX, new Usages4ClassProcessor()); + processors.put(USAGES_SUFFIX, new Usages4UsagesProcessor()); + walker.walk(new MainAnalyzer(cache, config, processors)); + usages.analyze(); + return new UsagesScanResult(usages); + } + + private class Usages4ClassProcessor implements Processor { + @Override + public void process(String className, InputStream in) throws IOException { + usages.parseClass(className, in); + } + } + + private class Usages4UsagesProcessor implements Processor { + @Override + public void process(String className, InputStream in) throws IOException { + usages.setNeedPostprocessing(false); + usages.getUsagesForClass(className).readFromStream(in); + } + } + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/UseKind.java b/server/src/main/java/com/devexperts/usages/analyzer/UseKind.java new file mode 100644 index 0000000..f60f863 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/UseKind.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer; + +public enum UseKind { + UNKNOWN(false), + EXTEND(true), + IMPLEMENT(true), + OVERRIDE(false), + SIGNATURE(false), + ANNOTATION(false), + THROW(false), + CATCH(false), + RETURN(false), + ARGUMENT(false), + CONSTANT(false), + FIELD(false), + NEW(false), + ANEWARRAY(false), + CHECKCAST(false), + INSTANCEOF(false), + GETFIELD(true), + PUTFIELD(true), + GETSTATIC(true), + PUTSTATIC(true), + INVOKEVIRTUAL(true), + INVOKESPECIAL(false), + INVOKESTATIC(true), + INVOKEINTERFACE(true), + INVOKEDYNAMIC(true); + + public final boolean inheritedUse; + + UseKind(boolean inheritedUse) { + this.inheritedUse = inheritedUse; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/concurrent/ConcurrentOutputStream.java b/server/src/main/java/com/devexperts/usages/analyzer/concurrent/ConcurrentOutputStream.java new file mode 100644 index 0000000..38d1364 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/concurrent/ConcurrentOutputStream.java @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.concurrent; + +import com.devexperts.usages.analyzer.FileUtils; + +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Provides opportunity to write concurrently into same file with wait-free guarantee, output from + * only a single thread will actually be written. File will not be seen before stream is closed. + *

+ * The main guarantee of this class is the following: when some threads write to the same file via + * this stream and no one tries to delete it, one of those threads will successfully create and fill + * the file. + *

+ * Note: while this stream is opened, some temporal file will exist in directory which + * contains target file. + *

+ * Note: instance of this class cannot be used concurrently. + */ +public class ConcurrentOutputStream extends OutputStream implements Closeable { + private final File targetFile; + + private final File tempFile; + + private final OutputStream os; + + private boolean failedToWrite = false; + + /** + * Creates a stream. If file already exists, writing to stream have no sense, so + * it is recommended to check target file presence on disk before calling this + * constructor. + * + * @throws IOException if I/O error occurs + */ + public ConcurrentOutputStream(String filename) throws IOException { + this(new File(filename)); + } + + /** + * Equivalent to calling {@code ConcurrentOutputStream}(file.getPath()). + */ + public ConcurrentOutputStream(File file) throws IOException { + targetFile = file; + tempFile = FileUtils.createTempFile(targetFile.toString()); + os = new FileOutputStream(tempFile); + } + + @Override + public void write(byte[] b) throws IOException { + try { + os.write(b); + } catch (IOException e) { + failedToWrite = true; + throw e; + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + try { + os.write(b, off, len); + } catch (IOException e) { + failedToWrite = true; + throw e; + } + } + + @Override + public void write(int b) throws IOException { + try { + os.write(b); + } catch (IOException e) { + failedToWrite = true; + throw e; + } + } + + @Override + public void flush() throws IOException { + try { + os.flush(); + } catch (IOException e) { + failedToWrite = true; + throw e; + } + } + + /** + * Closes the stream, target file appears at due location. + * + * @throws IOException + */ + @Override + public void close() throws IOException { + try { + os.close(); + if (!failedToWrite) { + tempFile.renameTo(targetFile); + } + } finally { + tempFile.delete(); + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/executors/ActionExecutionException.java b/server/src/main/java/com/devexperts/usages/analyzer/executors/ActionExecutionException.java new file mode 100644 index 0000000..65cd5a8 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/executors/ActionExecutionException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.executors; + +public class ActionExecutionException extends Exception { + private Exception cause; + + public ActionExecutionException() { + } + + public ActionExecutionException(String message) { + super(message); + } + + public ActionExecutionException(String message, Exception cause) { + super(message); + this.cause = cause; + } + + public ActionExecutionException(Exception cause) { + this(cause.toString()); + this.cause = cause; + } + + @Override + public Exception getCause() { + return cause; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/executors/CallerInterruptedException.java b/server/src/main/java/com/devexperts/usages/analyzer/executors/CallerInterruptedException.java new file mode 100644 index 0000000..568507e --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/executors/CallerInterruptedException.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.executors; + +public class CallerInterruptedException extends Exception { + private InterruptedException cause; + + public CallerInterruptedException(InterruptedException cause) { + this.cause = cause; + } + + @Override + public InterruptedException getCause() { + return cause; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/executors/DaemonThreadFactory.java b/server/src/main/java/com/devexperts/usages/analyzer/executors/DaemonThreadFactory.java new file mode 100644 index 0000000..94f5bf2 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/executors/DaemonThreadFactory.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.executors; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This is a copy of {@code Executors.DefaultThreadFactory} which produces daemons only + */ +class DaemonThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + DaemonThreadFactory() { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : + Thread.currentThread().getThreadGroup(); + namePrefix = "pool-" + + poolNumber.getAndIncrement() + + "-thread-"; + } + + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, + namePrefix + threadNumber.getAndIncrement(), + 0); + if (!t.isDaemon()) + t.setDaemon(true); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } +} + diff --git a/server/src/main/java/com/devexperts/usages/analyzer/executors/InsistentExecutor.java b/server/src/main/java/com/devexperts/usages/analyzer/executors/InsistentExecutor.java new file mode 100644 index 0000000..f1ab69b --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/executors/InsistentExecutor.java @@ -0,0 +1,358 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.executors; + +import org.apache.log4j.Logger; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + + +/** + * Attempts to perform a pack of actions. To execute an action, first create a slave, then use its + * submit method to perform actions. After all actions submitted, invoke await, unsafeAwait + * or complete to wait for execution completion, or finish to just free Slave as soon as it complete + * all tasks. + *

+ * All slaves share a same thread pool, but await for only their own actions. + *

+ * By default, if action failed, it will be repeated in at least repetitionDelay milliseconds. If second + * execution failed, await, unsafeAwait and complete throw. Exact scenarios description see + * in await. + *

+ * This behaviour may be changed, see InsistentExecutor.Action for details. + *

+ * Once all actions are performed and await or similar method is invoked, submit becomes to do + * nothing. So, submitting an action after await or similar method was invoked will not grantee its execution + * (excluding a case when action is submitted inside another action). + */ +public class InsistentExecutor { + private static final Logger logger = Logger.getLogger(InsistentExecutor.class); + + public static final int DEFAULT_THREAD_POOL_SIZE = 10; + public static final int THREAD_NUM_PER_SLAVE = 2; + + public static final long SLEEP_TIME_THRESHOLD_FOR_LOGGING = TimeUnit.SECONDS.toMillis(5); + public static final int DEFAULT_REPETITION_DELAY = (int) TimeUnit.SECONDS.toMillis(1); + + private final ThreadPoolExecutor executorService; + private final int repetitionDelay; + + private final Semaphore activeSlaves; + + private InsistentExecutor(ThreadPoolExecutor executorService, int repetitionDelay, int maxSlaveNum) { + this.executorService = executorService; + executorService.setThreadFactory(new DaemonThreadFactory()); + + this.repetitionDelay = repetitionDelay; + activeSlaves = new Semaphore(maxSlaveNum); + } + + public InsistentExecutor(int maximumPoolSize, int repetitionDelay, int maxSlaveNum) { + this( + new ThreadPoolExecutor( + maximumPoolSize + THREAD_NUM_PER_SLAVE * maxSlaveNum, + maximumPoolSize + THREAD_NUM_PER_SLAVE * maxSlaveNum, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingDeque() + ), + repetitionDelay, + maxSlaveNum + ); + } + + public InsistentExecutor(int threadNum, int repetitionDelay) { + this(threadNum, repetitionDelay, 1); + } + + public InsistentExecutor(int threadNum) { + this(threadNum, DEFAULT_REPETITION_DELAY); + } + + public InsistentExecutor() { + this(DEFAULT_THREAD_POOL_SIZE); + } + + + public Slave newSlave() { + return new Slave(); + } + + public Slave newSlave(int availableThreads) { + return new Slave(availableThreads); + } + + public void shutdown() { + executorService.shutdown(); + } + + private static final AtomicInteger working = new AtomicInteger(); + + + + /** + * Allows submitting tasks and wait until all of them are finished + */ + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + public class Slave { + private final AtomicBoolean awaited = new AtomicBoolean(false); + + /** + * At any time equals to (submitted task number - finished task number + !awaited) + */ + private final AtomicInteger activeMeter = new AtomicInteger(1); + + /** + * Gate opens when activeMeter == 0, hence awaiting threads get released + */ + private CountDownLatch finished = new CountDownLatch(1); + + /** + * Not null if slave completely failed + */ + private AtomicReference actionException = new AtomicReference(); + + private final ActionSubmitter actionFirstQueue; + private final ActionSubmitter actionsRepetitionQueue; + + /** + * Limits number of executing actions + */ + private final Semaphore activeActionsSemaphore; + + private Slave(int activeNum) { + this(new Semaphore(activeNum)); + } + + private Slave() { + this(new NullSemaphore()); + } + + private Slave(Semaphore activeActionsSemaphore) { + this.activeActionsSemaphore = activeActionsSemaphore; + activeSlaves.acquireUninterruptibly(); + actionFirstQueue = new ActionSubmitter(this.activeActionsSemaphore); + actionsRepetitionQueue = new ActionSubmitter(this.activeActionsSemaphore); + } + + public void submit(Action action) { + if (actionException.get() == null) { + activeMeter.incrementAndGet(); + actionFirstQueue.submit(new ActionRunnable(action, true, 0)); + } + } + + private void decrementActiveMeter() { + if (activeMeter.decrementAndGet() == 0) { + actionFirstQueue.stop(); + actionsRepetitionQueue.stop(); + activeSlaves.release(); + + finished.countDown(); + } + } + + public void finish() { + if (awaited.compareAndSet(false, true)) + decrementActiveMeter(); + } + + public void await() throws InterruptedException, ActionExecutionException { + finish(); + try { + finished.await(); + } catch (InterruptedException e) { + if (!actionException.compareAndSet(null, new CallerInterruptedException(e))) { + // when not throwing InterruptedException, set interrupted flag + Thread.currentThread().interrupt(); + } + } + + Throwable e = this.actionException.get(); + if (e == null) + return; + + if (e instanceof CallerInterruptedException) + throw ((CallerInterruptedException) e).getCause(); + + if (e instanceof Exception) + throw new ActionExecutionException((Exception) e); + + if (e instanceof Error) + throw (Error) e; + + else + throw new RuntimeException("Throwable in action occurred", e); + + } + + public void awaitUninterruptedly() throws ActionExecutionException { + try { + await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void complete() throws ActionExecutionException, InterruptedException { + try { + await(); + } finally { + InsistentExecutor.this.shutdown(); + } + } + + public InsistentExecutor getFactory() { + return InsistentExecutor.this; + } + + private class ActionRunnable implements Runnable { + private final Action action; + private final boolean firstTime; + private final long executeTime; + + public ActionRunnable(Action action, boolean firstTime, long executeTime) { + this.action = action; + this.firstTime = firstTime; + this.executeTime = executeTime; + } + + @Override + public void run() { + try { + if (actionException.get() != null) { + decrementActiveMeter(); + return; + } + try { + if (finished.getCount() != 0) { + action.run(); + decrementActiveMeter(); + } + } catch (Exception e) { + //noinspection unchecked + if (firstTime) { + action.onFirstFail(e); + actionsRepetitionQueue.submit(new ActionRunnable(action, false, System.currentTimeMillis() + repetitionDelay)); + } else { + action.onRepeatedFail(e); + decrementActiveMeter(); + } + } + } catch (Throwable e) { + actionException.compareAndSet(null, e); + decrementActiveMeter(); + } finally { + activeActionsSemaphore.release(); + } + } + + } + + /** + * Submits actions for execution, waiting if necessary + */ + private class ActionSubmitter implements Runnable { + private final BlockingDeque queue = new LinkedBlockingDeque(); + + private final Semaphore semaphore; + + private final Future future; + + public ActionSubmitter(Semaphore semaphore) { + this.semaphore = semaphore; + future = executorService.submit(this); + } + + public void submit(ActionRunnable actionRunnable) { + queue.add(actionRunnable); + } + + public void stop() { + future.cancel(true); + } + + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + Slave.ActionRunnable action = queue.take(); + + long remainingTime = action.executeTime - System.currentTimeMillis(); + if (remainingTime > 0) { + if (remainingTime > SLEEP_TIME_THRESHOLD_FOR_LOGGING) { + logger.info(String.format("Next repetition in %.1f sec.", remainingTime / 1e3)); + } + Thread.sleep(remainingTime); + } + + semaphore.acquire(); + executorService.submit(action); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + working.decrementAndGet(); + } + } + + } + + + /** + * Action to perform. + */ + public abstract static class Action { + + /** + * Performed operation. + */ + public abstract void run() throws Exception; + + /** + * Invoked when action failed at first time. It is preferably to override this method with more particular + * operation. You may throw from this method to stop processing remaining actions, exception will be rethrown + * from await method. + * + * @param e exception thrown by action + */ + public void onFirstFail(Exception e) throws Exception { + logger.warn("Action failed, going to launch again later", e); + } + + /** + * Invoked when action failed again. It is preferably to override this method with more particular operation. + * Possibly you would like to throw an exception here, it will be rethrown by await method and + * remaining actions will not be performed. Otherwise, await will complete normally. + * + * @param e exception thrown by action + */ + public void onRepeatedFail(Exception e) throws Exception { + throw e; + } + + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/executors/NullSemaphore.java b/server/src/main/java/com/devexperts/usages/analyzer/executors/NullSemaphore.java new file mode 100644 index 0000000..285cd02 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/executors/NullSemaphore.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.executors; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * Acts just like {@link Semaphore} with infinite permission number + */ +public class NullSemaphore extends Semaphore { + public NullSemaphore() { + super(0); + } + + @Override + public void acquire() throws InterruptedException { + } + + @Override + public void acquireUninterruptibly() { + } + + @Override + public boolean tryAcquire() { + return true; + } + + @Override + public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public void release() { + } + + @Override + public void acquire(int permits) throws InterruptedException { + } + + @Override + public void acquireUninterruptibly(int permits) { + } + + @Override + public boolean tryAcquire(int permits) { + return true; + } + + @Override + public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public void release(int permits) { + } + + @Override + public int availablePermits() { + return Integer.MAX_VALUE; + } + + @Override + public int drainPermits() { + return 0; + } + + @Override + protected void reducePermits(int reduction) { + } + + @Override + public boolean isFair() { + return false; + } + + @Override + protected Collection getQueuedThreads() { + return Collections.emptyList(); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/internal/MemberInternal.java b/server/src/main/java/com/devexperts/usages/analyzer/internal/MemberInternal.java new file mode 100644 index 0000000..e38d8ff --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/internal/MemberInternal.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.internal; + +import com.devexperts.usages.api.Member; +import org.objectweb.asm.Type; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class MemberInternal implements Comparable { + public static final String CLASS_MEMBER_NAME = ""; + + private final String className; + private final String memberName; + + public MemberInternal(String className, String memberName) { + this.className = className; + this.memberName = memberName; + } + + public static MemberInternal valueOf(String memberFullName) { + int hashIndex = memberFullName.lastIndexOf("#"); + return new MemberInternal(memberFullName.substring(0, hashIndex), memberFullName.substring(hashIndex + 1)); + } + + public static String methodMemberName(String methodName, Type type) { + StringBuilder sb = new StringBuilder(); + sb.append(methodName); + sb.append('('); + Type[] argumentTypes = type.getArgumentTypes(); + for (int i = 0; i < argumentTypes.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(argumentTypes[i].getClassName()); + } + sb.append(')'); + return sb.toString(); + } + + public Member toMember() { + if (memberName.equals(CLASS_MEMBER_NAME)) { + return Member.Companion.fromClass(className); + } + int i = memberName.indexOf('('); + if (i < 0) { // field + if (memberName.equals("")) + return Member.Companion.fromMethod(className, memberName, Collections.emptyList()); + return Member.Companion.fromField(className, memberName); + } + String mname = memberName.substring(0, i); + List params = Arrays.asList(memberName.substring(i + 1, memberName.length() - 1).split(",")); + return Member.Companion.fromMethod(className, mname, params); + } + + public String getClassName() { + return className; + } + + public String getMemberName() { + return memberName; + } + + public int compareTo(MemberInternal o) { + int i = className.compareTo(o.className); + if (i != 0) { + return i; + } + return memberName.compareTo(o.memberName); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MemberInternal)) { + return false; + } + MemberInternal member = (MemberInternal) o; + return className.equals(member.className) && memberName.equals(member.memberName); + + } + + @Override + public int hashCode() { + return 31 * className.hashCode() + memberName.hashCode(); + } + + @Override + public String toString() { + return className + "#" + memberName; + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/tune/AnalyzeSingleUsagesKeeper.java b/server/src/main/java/com/devexperts/usages/analyzer/tune/AnalyzeSingleUsagesKeeper.java new file mode 100644 index 0000000..df19113 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/tune/AnalyzeSingleUsagesKeeper.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.tune; + +import com.devexperts.usages.analyzer.Cache; +import com.devexperts.usages.analyzer.ClassUsages; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class AnalyzeSingleUsagesKeeper implements UsagesKeeper { + private final Map interestingClasses; + private final UsagesKeeper keeper; + + public AnalyzeSingleUsagesKeeper(String clazz, Cache cache) { + this(clazz, cache, new HashMap()); + } + + public AnalyzeSingleUsagesKeeper(String clazz, Cache cache, Map container) { + this.interestingClasses = Collections.singletonMap(clazz, new ClassUsages(cache, clazz)); + this.keeper = new SimpleUsagesKeeper(container, cache); + } + + @Override + public ClassUsages get(String clazz) { + return keeper.get(clazz); + } + + @Override + public ClassUsages getOrEmpty(String clazz) { + return keeper.getOrEmpty(clazz); + } + + @Override + public Set> allClassUsages() { + return interestingClasses.entrySet(); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/tune/SimpleUsagesKeeper.java b/server/src/main/java/com/devexperts/usages/analyzer/tune/SimpleUsagesKeeper.java new file mode 100644 index 0000000..efba064 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/tune/SimpleUsagesKeeper.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.tune; + +import com.devexperts.usages.analyzer.Cache; +import com.devexperts.usages.analyzer.ClassUsages; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class SimpleUsagesKeeper implements UsagesKeeper { + private final Map usages; + private final Cache cache; + + public SimpleUsagesKeeper(Map container, Cache cache) { + this.usages = container; + this.cache = cache; + } + + public SimpleUsagesKeeper(Cache cache) { + this(new HashMap(), cache); + } + + @Override + public ClassUsages get(String className) { + return usages.get(className); + } + + @Override + public ClassUsages getOrEmpty(String className) { + ClassUsages result = usages.get(className); + if (result == null) { + if (className.endsWith("]") || className.endsWith(";") || className.contains("/")) + throw new AssertionError("Not a class name: " + className); + usages.put(cache.resolveString(className), result = new ClassUsages(cache, className)); + } + return result; + } + + @Override + public Set> allClassUsages() { + return usages.entrySet(); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/tune/UsagesKeeper.java b/server/src/main/java/com/devexperts/usages/analyzer/tune/UsagesKeeper.java new file mode 100644 index 0000000..844eca7 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/tune/UsagesKeeper.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.tune; + +import com.devexperts.usages.analyzer.ClassUsages; + +import java.util.Map; +import java.util.Set; + +public interface UsagesKeeper { + ClassUsages get(String clazz); + + ClassUsages getOrEmpty(String clazz); + + Set> allClassUsages(); +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/FileAnalyzer.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/FileAnalyzer.java new file mode 100644 index 0000000..129c8b5 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/FileAnalyzer.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker; + +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +import java.io.IOException; + +public interface FileAnalyzer { + void process(I info) throws IOException; +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfo.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfo.java new file mode 100644 index 0000000..d09977c --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfo.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.info; + +import java.io.IOException; +import java.io.InputStream; + +public interface FileInfo { + String getName(); + + String getPath(); + + String getSuffix(); + + String getExtension(); + + String getBaseName(); + + InputStream openInputStream() throws IOException; + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfoBase.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfoBase.java new file mode 100644 index 0000000..5959a28 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/FileInfoBase.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.info; + +import java.io.IOException; +import java.io.InputStream; + +abstract class FileInfoBase implements FileInfo { + public abstract String getName(); + + public abstract String getPath(); + + public String getSuffix() { + String name = getName(); + int i = name.lastIndexOf('.'); + return i == -1 ? "" : name.substring(i); + } + + public String getExtension() { + String name = getName(); + return name.substring(name.lastIndexOf('.') + 1); + } + + public String getBaseName() { + String name = getName(); + int i = name.lastIndexOf('.'); + return i == -1 ? name : name.substring(0, i); + } + + public abstract InputStream openInputStream() throws IOException; + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/info/PlainFileInfo.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/PlainFileInfo.java new file mode 100644 index 0000000..8b4cfdf --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/PlainFileInfo.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.info; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * For files in filesystem. + */ +public class PlainFileInfo extends FileInfoBase { + private final File file; + + public PlainFileInfo(File file) { + this.file = file; + } + + public File getFile() { + return file; + } + + @Override + public String getName() { + return file.getName(); + } + + @Override + public String getPath() { + return file.getPath(); + } + + @Override + public InputStream openInputStream() throws IOException { + return new FileInputStream(file); + } + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/info/ZipEntryInfo.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/ZipEntryInfo.java new file mode 100644 index 0000000..580512e --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/info/ZipEntryInfo.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.info; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ZipEntryInfo extends FileInfoBase { + + /** + * Full path of zip entry, for example "./usages.zip!org/log4j.usages" + */ + private final String fullPath; + + private final ZipFile zipFile; + + private final ZipEntry zipEntry; + + public ZipEntryInfo(String fullPath, ZipFile zipFile, ZipEntry zipEntry) { + this.fullPath = fullPath; + this.zipFile = zipFile; + this.zipEntry = zipEntry; + } + + @Override + public String getName() { + return zipEntry.getName(); + } + + @Override + public String getPath() { + return fullPath; + } + + public InputStream openInputStream() throws IOException { + return zipFile.getInputStream(zipEntry); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Delegating.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Delegating.java new file mode 100644 index 0000000..6a5eb7b --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Delegating.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +/** + * Allows walkers pass files to other walkers. + * + * @param type of fileInfo which the walker passes to next walker + * @param type of fileInfo which would be passed to processor at last layer + */ +public interface Delegating { + Walker makeDelegate(K info); +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/DelegatingWalker.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/DelegatingWalker.java new file mode 100644 index 0000000..5d9a1f8 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/DelegatingWalker.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +/** + * Can be used at passing (left) side of link in delegation chain. + * + * @param type of fileInfo which it provides directly to lower layer + * @param type of fileInfo which fileAnalyzer accepts + */ +public abstract class DelegatingWalker implements Walker { + protected final Delegating delegating; + + public DelegatingWalker(Delegating delegating) { + this.delegating = delegating; + } + +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/EmptyWalker.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/EmptyWalker.java new file mode 100644 index 0000000..d9c04be --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/EmptyWalker.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +import java.io.IOException; + +public class EmptyWalker implements Walker { + @Override + public void walk(FileAnalyzer fileAnalyzer) throws IOException { + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/FilePackWalker.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/FilePackWalker.java new file mode 100644 index 0000000..9b2b2f9 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/FilePackWalker.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; +import com.devexperts.usages.analyzer.walker.info.PlainFileInfo; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Walks through specified files. + * + * @param type of fileInfo passed to analyser at the end of delegation chain + */ +public class FilePackWalker extends DelegatingWalker { + private final Collection infos; + + public FilePackWalker(Collection infos, Delegating delegating) { + super(delegating); + this.infos = infos; + } + + public static FilePackWalker fromFiles( + Collection files, Delegating delegating) { + ArrayList infos = new ArrayList(); + for (File file : files) { + infos.add(new PlainFileInfo(file)); + } + return new FilePackWalker(infos, delegating); + } + + public static FilePackWalker fromFilenames( + Collection filenames, Delegating delegating) { + ArrayList infos = new ArrayList(); + for (String filename : filenames) { + infos.add(new PlainFileInfo(new File(filename))); + } + return new FilePackWalker(infos, delegating); + } + + @Override + public void walk(FileAnalyzer analyzer) throws IOException { + for (N info : infos) { + delegating.makeDelegate(info) + .walk(analyzer); + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/MidDelegating.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/MidDelegating.java new file mode 100644 index 0000000..543ef1a --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/MidDelegating.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +/** + * Delegating which appears not at the end of the chain. + * + * @param type of fileInfo which the walker passes to next walker + * @param type of fileInfo directly passed to next layer by next walker + * @param type of fileInfo which would be passed to processor at last layer + */ +public interface MidDelegating extends Delegating { + DelegatingWalker makeDelegate(K info); +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/PackWalker.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/PackWalker.java new file mode 100644 index 0000000..d3bb2b2 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/PackWalker.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +import java.io.IOException; +import java.util.Collection; + +/** + * Unites several walkers into one + */ +public class PackWalker implements Walker { + private final Collection> walkers; + + public PackWalker(Collection> walkers) { + this.walkers = walkers; + } + + @Override + public void walk(FileAnalyzer fileAnalyzer) throws IOException { + for (Walker walker : walkers) { + walker.walk(fileAnalyzer); + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Passing.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Passing.java new file mode 100644 index 0000000..b1e6559 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Passing.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +/** + * This class facilitates Delegating's creation. + * + * @param available info types which can be passed to Delegating. + * @param info type which delegate walker directly passes to next layer. + */ +public abstract class Passing { + public abstract MidDelegating delegateTo( + Delegating nextDelegating); + + public MidDelegating passToProcessor() { + return delegateTo(TerminalWalker.getDelegating()); + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/TerminalWalker.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/TerminalWalker.java new file mode 100644 index 0000000..9f2b34e --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/TerminalWalker.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +import java.io.IOException; + +/** + * Just passes given info to processor, thus taking role of terminate walker in delegation chain. + * + * @param type of operated fileInfo + */ +public class TerminalWalker implements Walker { + private final I fileInfo; + + public TerminalWalker(I fileInfo) { + this.fileInfo = fileInfo; + } + + @Override + public void walk(FileAnalyzer fileAnalyzer) throws IOException { + fileAnalyzer.process(fileInfo); + } + + + // often used + private static final Delegating DELEGATING = new TheDelegating(); + + public static Delegating getDelegating() { + //noinspection unchecked + return DELEGATING; + } + + private static class TheDelegating implements Delegating { + @Override + public Walker makeDelegate(I info) { + return new TerminalWalker(info); + } + } +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Walker.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Walker.java new file mode 100644 index 0000000..a83bac3 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/Walker.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; + +import java.io.IOException; + +/** + * Visits some files, passing them to analyzer. + * There is no need to implement this class, implement SimpleWalker or DelegatingWalker instead. + * + * @param type of fileInfo which it provides directly to lower layer + * @param type of fileInfo which fileAnalyzer accepts + */ +public interface Walker { + void walk(FileAnalyzer fileAnalyzer) throws IOException; +} diff --git a/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/ZipRecursiveWalker.java b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/ZipRecursiveWalker.java new file mode 100644 index 0000000..bb3cfa9 --- /dev/null +++ b/server/src/main/java/com/devexperts/usages/analyzer/walker/walkers/ZipRecursiveWalker.java @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.analyzer.walker.walkers; + +import com.devexperts.usages.analyzer.walker.FileAnalyzer; +import com.devexperts.usages.analyzer.walker.info.FileInfo; +import com.devexperts.usages.analyzer.walker.info.PlainFileInfo; +import com.devexperts.usages.analyzer.walker.info.ZipEntryInfo; +import org.apache.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Walks through zip entries. When encounters a zip inside, makes it out too. + * + * @param type of fileInfo passed to analyser at the end of delegation chain + */ +public class ZipRecursiveWalker extends DelegatingWalker { + private static final Logger logger = Logger.getLogger(ZipRecursiveWalker.class); + + private final PlainFileInfo rootInfo; + + public ZipRecursiveWalker(PlainFileInfo file, Delegating delegating) { + super(delegating); + this.rootInfo = file; + } + + public ZipRecursiveWalker(File file, Delegating delegating) { + this(new PlainFileInfo(file), delegating); + } + + public static ZipRecursiveWalker ofFile(File file) { + return new ZipRecursiveWalker<>(file, TerminalWalker.getDelegating()); + } + + @Override + public void walk(FileAnalyzer analyzer) throws IOException { + final ZipFile zip = new ZipFile(rootInfo.getPath()); + try { + for (Enumeration en = zip.entries(); en.hasMoreElements(); ) { + final ZipEntry ze = en.nextElement(); + if (ze.isDirectory()) { + continue; + } + String entryName = ze.getName(); + String entryPath = rootInfo.getPath() + "!" + entryName; + if (entryName.endsWith(".zip") || entryName.endsWith(".jar") || entryName.endsWith(".war")) { + File temp = new File("temp" + entryPath.replaceAll("[\\\\!/]", "~") + ".zip"); + logger.debug("Extracting " + entryPath + " to " + temp); + + try { + Files.copy(zip.getInputStream(ze), temp.toPath()); + new ZipRecursiveWalker(temp, delegating) + .walk(analyzer); + } finally { + temp.delete(); + } + } + this.delegating.makeDelegate(new ZipEntryInfo(entryPath, zip, ze)) + .walk(analyzer); + } + } finally { + try { + zip.close(); + } catch (Throwable ignored) { + } + } + } + + public static class D extends Passing { + @Override + public MidDelegating delegateTo( + final Delegating nextDelegating) + { + return new MidDelegating() { + @Override + public DelegatingWalker makeDelegate(PlainFileInfo info) { + return new ZipRecursiveWalker(info, nextDelegating); + } + }; + } + } +} diff --git a/server/src/main/kotlin/com/devexperts/usages/server/Database.kt b/server/src/main/kotlin/com/devexperts/usages/server/Database.kt new file mode 100644 index 0000000..b233d7f --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/Database.kt @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server + +import com.devexperts.usages.api.MemberType +import com.devexperts.usages.api.UsageKind +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils.create +import org.jetbrains.exposed.sql.SchemaUtils.drop +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction +import java.sql.Connection + +private val TABLES = arrayOf(Artifacts, ArtifactStatus, Packages, ArtifactPackages, Dependencies, ArtifactSources, + Members, Derived, Locations, MemberUsages, MemberStructure); + +fun initDatabase(file: String) { + initDatabaseByFullUrl("jdbc:h2:file:$file") +} + +fun initInMemoryDatabase() { + initDatabaseByFullUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1") +} + +fun dropDatabase() = transaction { drop(*TABLES) } + +private fun initDatabaseByFullUrl(url: String) { + Database.connect(url = url, driver = "org.h2.Driver") + TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE + transaction { create(*TABLES) } +} + +// == ARTIFACT MANAGER == + +object Artifacts : Table() { + val id = integer("id").autoIncrement().primaryKey() + val groupId = varchar("groupId", 255) + val artifactId = varchar("artifactId", 255) + val version = varchar("version", 255) + val type = varchar("type", 255).nullable() + val classifier = varchar("classifier", 255).nullable() + + init { + uniqueIndex(groupId, artifactId, version, type, classifier) + } +} + +object Packages : Table() { + val id = integer("id").autoIncrement().primaryKey() + val pkg = varchar("package", 255).index() +} + +object ArtifactPackages : Table() { + val artifactId = (integer("artifactId") references Artifacts.id).primaryKey().index() + val packageId = (integer("packageId") references Packages.id).primaryKey().index() +} + +object ArtifactSources : Table() { + val artifactId = (integer("artifactId") references Artifacts.id).primaryKey() + val indexerId = varchar("indexerId", 255) +} + +object Dependencies : Table() { + val artifactId = (integer("artifactId") references Artifacts.id).primaryKey().index() + val dependencyArtifactId = (integer("dependencyArtifactId") references Artifacts.id).primaryKey().index() +} + +// == ANALYZER == + +object Members : Table() { + val id = integer("id").autoIncrement().primaryKey() + val qualifiedName = varchar("qualifiedName", 255) + val paramTypes = varchar("paramTypes", 512) + val type = enumeration("type", MemberType::class.java) + + init { + // todo uniqueIndex(qualifiedName, paramTypes, type) + } +} + +object Locations : Table() { + val id = integer("id").autoIncrement().primaryKey() + val artifactId = (integer("artifactId") references Artifacts.id).index() + val memberId = (integer("memberId") references Members.id).index() + val file = varchar("file", 255).nullable() + val line = integer("line").nullable() + + init { + // todo uniqueIndex(artifactId, memberId, file, line) + } +} + +object MemberUsages : Table() { + val memberId = (integer("memberId") references Members.id).primaryKey().index() + val usageKind = enumeration("usageKind", UsageKind::class.java).primaryKey() + val locationId = (integer("locationId") references Locations.id).primaryKey() +} + +// == CODE STRUCTURE == + +object Derived : Table() { + val memberId = (integer("memberId") references Members.id).primaryKey().index() // class or method + val derivedMemberId = (integer("derivedMemberId") references Members.id).primaryKey() // derived class or method +} + +object MemberStructure : Table() { + val memberId = (integer("memberId") references Members.id).primaryKey().index() // package or class + val internalMemberId = (integer("internalMemberId") references Members.id).primaryKey() // class, or method, or field in the member +} + +// == COMMON == + +object ArtifactStatus : Table() { + val artifactId = (integer("artifactId") references Artifacts.id).primaryKey() + val analyzed = bool("analyzed") // true if this artifact has been analyzed + val hasPackages = bool("hasPackages") // true if packages information has been filled for this artifact todo supporting +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/Server.kt b/server/src/main/kotlin/com/devexperts/usages/server/Server.kt new file mode 100644 index 0000000..8dee03c --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/Server.kt @@ -0,0 +1,187 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server + +import com.devexperts.logging.Logging +import com.devexperts.usages.analyzer.Analyzer0 +import com.devexperts.usages.api.* +import com.devexperts.usages.server.artifacts.ArtifactManager +import com.devexperts.usages.server.config.Configuration +import com.devexperts.usages.server.config.readSettings +import com.devexperts.usages.server.indexer.MavenIndexer +import com.devexperts.usages.server.indexer.createIndexers +import org.eclipse.aether.util.version.GenericVersionScheme +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Flux +import java.util.concurrent.Executors +import java.util.regex.Pattern +import java.util.stream.Collectors +import java.util.stream.Stream +import kotlin.concurrent.fixedRateTimer +import kotlin.streams.toList + +@SpringBootApplication +class Application + +fun main(args: Array) { + initDatabase(Configuration.dbFile) +// Server.scheduleScan() + SpringApplication.run(Application::class.java, *args) +} + +private val log = Logging.getLogging(Server::class.java) + +object Server { + val indexerPool = Executors.newFixedThreadPool(1) + + val settings = readSettings() + val indexers = settings.createIndexers() + val indexerIdToIndexerMap: Map + + init { + indexerIdToIndexerMap = indexers.stream().collect(Collectors.toMap(MavenIndexer::id, { i -> i })) + } + + fun scheduleScan() { + indexers.forEach { indexer -> + fixedRateTimer(name = "MavenIndexer-${indexer.id}", period = indexer.scanDelay.time) { + indexerPool.submit { indexer.scan() } + } + } + } +} + +@RestController +class RestController { + + + +// fun cancel(@RequestHeader(UUID_HEADER_NAME) uuid: String) { +// TODO("Cancellation is not implemented") +// } + + @PostMapping(produces = arrayOf(MediaType.APPLICATION_STREAM_JSON_VALUE), value = "/usages") + fun findUsages(@RequestHeader(UUID_HEADER_NAME) uuid: String, @RequestBody request: MemberUsageRequest): Flux { + println("[$uuid] REQUEST=$request") + val pkg = request.member.packageName() + val artifactsWithPackage = ArtifactManager.artifactsWithPackage(pkg) + var artifactsToAnalyze = (artifactsWithPackage + ArtifactManager.artifactIdsWithAnyDependency(artifactsWithPackage)) + .map { artifactId -> ArtifactManager.getArtifact(artifactId) } + artifactsToAnalyze = filterArtifacts(artifactsToAnalyze, request.searchScope) +// println("[$uuid] ARTIFACTS TO ANALYZE=" + artifactsToAnalyze.map { it.value }) + // Analyze artifacts if needed + var isCancelled = false + val stream = artifactsToAnalyze.stream().parallel().map { artifact -> + if (isCancelled) + return@map Stream.empty() + val indexer = Server.indexerIdToIndexerMap[ArtifactManager.getSourceIndexerName(artifact.id)] + if (indexer == null) { + log.warn("Artifact ${artifact.value} has not been indexed yet and cannot be analyzed") + return@map Stream.empty() + } + val usages = Analyzer0().analyze(indexer, artifact.value) +// println("[$uuid] ARTIFACT ${artifact.value} ANALYZED, USAGES=$usages") + usages.stream() +// Analyzer.analyzeIfNeeded(indexer, artifact) + }.flatMap { it }.filter { usage -> + if (usage.member == request.member) + return@filter true + if (request.member.type == MemberType.PACKAGE) { + if (request.findClasses && usage.member.type == MemberType.CLASS && usage.member.packageName() == request.member.packageName()) + return@filter true + if (request.findClasses && request.findFields && usage.member.type == MemberType.FIELD && usage.member.packageName() == request.member.packageName()) + return@filter true + if (request.findClasses && request.findMethods && usage.member.type == MemberType.METHOD && usage.member.packageName() == request.member.packageName()) + return@filter true + } + if (request.member.type == MemberType.CLASS) { + if (request.findFields && usage.member.type == MemberType.FIELD && usage.member.className() == request.member.className()) + return@filter true + if (request.findMethods && usage.member.type == MemberType.METHOD && usage.member.className() == request.member.className()) + return@filter true + } + return@filter false + } + return Flux.fromStream(stream).doOnCancel { + isCancelled = true + } + // todo internal members (fields, methods) and derived members (derived classes, overridden methods) + } +} + +private fun filterArtifacts(artifacts: List>, searchScope: ArtifactMask): List> { + // Filter artifacts by coordinates (ignore last versions number restriction here) + val groupIdRegex = globPattern(searchScope.groupId) + val artifactIdRegex = globPattern(searchScope.artifactId) + val classifierRegex = globPattern(searchScope.classifier) + val packagingRegex = globPattern(searchScope.packaging) + val versionRegex = globPattern(searchScope.version) + val goodArtifacts = artifacts.stream().filter { + val a = it.value + groupIdRegex.matches(a.groupId) + && artifactIdRegex.matches(a.artifactId) + && classifierRegex.matches(a.classifier.toString()) // toString here because it can be null + && packagingRegex.matches(a.type.toString()) // toString here because it can be null + && versionRegex.matches(a.version) + } + // Return filtered artifacts if they should not be restricted by last versions number + if (searchScope.numberOfLastVersions < 0) + return goodArtifacts.toList() + // Split artifacts to lists with same coordinates except for version. + // Therefore, every list contains all versions of artifact. + data class ArtifactKey(val groupId: String, val artifactId: String, val classifier: String?, val type: String?) + + val artifactsByKey = goodArtifacts.collect( + Collectors.groupingBy, ArtifactKey> { + val a = it.value + ArtifactKey(groupId = a.groupId, artifactId = a.artifactId, + classifier = a.classifier, type = a.type) + }) + // Restrict by last versions number + val versionScheme = GenericVersionScheme() + return artifactsByKey.values.stream().flatMap { + it.stream().sorted({ a1, a2 -> + // todo do not create versions on each comparision! + val v1 = versionScheme.parseVersion(a1.value.version) + val v2 = versionScheme.parseVersion(a2.value.version) + v2.compareTo(v1) + }).limit(searchScope.numberOfLastVersions.toLong()) + }.toList() +} + +// todo verify this transformation +private fun globPattern(glob: String): Regex { + val regex = StringBuilder() + for (i in 0 until glob.length) { + val c = glob[i] + when (c) { + '*' -> regex.append(".*") + ',' -> regex.append('|') + else -> regex.append(Pattern.quote(glob.substring(i, i + 1))) + } + } + return Pattern.compile(regex.toString()).toRegex() +} + +data class WithId(val id: Int, val value: T) \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/analyzer/Analyzer.kt b/server/src/main/kotlin/com/devexperts/usages/server/analyzer/Analyzer.kt new file mode 100644 index 0000000..7b80925 --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/analyzer/Analyzer.kt @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.analyzer + +import com.devexperts.logging.Logging +import com.devexperts.usages.api.Artifact +import com.devexperts.usages.server.WithId +import com.devexperts.usages.server.artifacts.ArtifactManager +import com.devexperts.usages.server.indexer.MavenIndexer +import org.objectweb.asm.ClassReader +import java.util.zip.ZipFile + +object Analyzer { + private val log = Logging.getLogging(Analyzer::class.java) + + fun analyzeIfNeeded(indexer: MavenIndexer, artifact: WithId) { + if (!ArtifactManager.isAnalyzed(artifact.id)) { + downloadAndAnalyze(indexer, artifact) + ArtifactManager.markAnalyzed(artifact.id) + } + } + + private fun downloadAndAnalyze(indexer: MavenIndexer, artifact: WithId) { + val artifactFile = indexer.downloadArtifact(artifact.value) + if (artifactFile == null) { + log.error("Cannot analyze artifact $artifact, not downloaded") + return + } + ZipFile(artifactFile).use { zip -> + zip.entries().iterator().forEach { zipEntry -> + if (zipEntry.isDirectory) + return@forEach + if (zipEntry.name.endsWith(".class")) { + zip.getInputStream(zipEntry).use { classInputStream -> + val cr = ClassReader(classInputStream) + cr.accept(ClassAnalyzer(artifact.id), ClassReader.SKIP_FRAMES) + } + } + } + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/analyzer/ClassAnalyzer.kt b/server/src/main/kotlin/com/devexperts/usages/server/analyzer/ClassAnalyzer.kt new file mode 100644 index 0000000..3ef156b --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/analyzer/ClassAnalyzer.kt @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.analyzer + +import com.devexperts.usages.api.Member +import com.devexperts.usages.api.MemberType +import com.devexperts.usages.api.UsageKind +import com.devexperts.usages.server.WithId +import org.objectweb.asm.* +import org.objectweb.asm.signature.SignatureReader +import org.objectweb.asm.signature.SignatureVisitor +import java.io.File + +class ClassAnalyzer(private val artifactId: Int) : ClassVisitor(ASM_VERSION) { + private lateinit var classMember: WithId + + // Location parameters + private lateinit var locationMember: WithId + private var file: String? = null // defined in [visitSource] + private var lineNumber = -1 + + private fun addUsage(member: Member, usageKind: UsageKind) { + addUsage(UsagesManager.geOrCreateMember(member), usageKind) + } + + private fun addUsage(member: WithId, usageKind: UsageKind) { + UsagesManager.addMemberUsage(member.id, + UsagesManager.getOrCreateLocationId(artifactId, locationMember.id, file, lineNumber), + usageKind) + } + + private fun addTypeUsage(type: Type, usageKind: UsageKind) { + var myType = type + if (myType.sort == Type.METHOD) { + addTypeUsage(myType.returnType, UsageKind.METHOD_RETURN) + for (arg in myType.argumentTypes) + addTypeUsage(arg, UsageKind.METHOD_PARAMETER) + return + } + while (myType.sort == Type.ARRAY) + myType = myType.elementType + if (myType.sort == Type.OBJECT) + addUsage(Member.fromClass(myType.className), usageKind) + } + + private fun addHandleUsage(handle: Handle, usageKind: UsageKind) { + + } + + private fun addConstantUsage(constant: Any, usageKind: UsageKind) { + when (constant) { + is Type -> addTypeUsage(constant, usageKind) + is Handle -> addHandleUsage(constant, usageKind) + } + } + + private fun processSignature(signature: String?) { + if (signature != null) + SignatureReader(signature).accept(SignatureAnalyzer()) + } + + override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array?) { + val m = Member(toClassName(name), emptyList(), MemberType.CLASS) + classMember = UsagesManager.geOrCreateMember(m) + locationMember = classMember + addUsage(classMember.value, UsageKind.CLASS_DECLARATION) + processSignature(signature) + } + + override fun visitEnd() { + // todo location at package??? + } + + override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor { + val fieldMember = Member.fromField(classMember.value.qualifiedMemberName, name) + addTypeUsage(Type.getType(desc), UsageKind.FIELD) + processSignature(signature) + return super.visitField(access, name, desc, signature, value) + } + + override fun visitSource(source: String?, debug: String?) { + // Define [file] here + if (source == null) + return + var className = classMember.value.qualifiedMemberName + val lastDotIndex = className.lastIndexOf('.') + if (lastDotIndex > 0) + className = className.substring(0, lastDotIndex) + file = className.replace('.', File.separatorChar) + File.separator + source + } + + private inner class SignatureAnalyzer : SignatureVisitor(ASM_VERSION) { + override fun visitClassType(internalName: String) = addUsage( + Member.fromClass(toClassName(internalName)), UsageKind.SIGNATURE) + } +} + +fun toClassName(internalName: String) = internalName.replace('/', '.') + +private val ASM_VERSION = Opcodes.ASM6 \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/analyzer/UsagesManager.kt b/server/src/main/kotlin/com/devexperts/usages/server/analyzer/UsagesManager.kt new file mode 100644 index 0000000..6c7d564 --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/analyzer/UsagesManager.kt @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.analyzer + +import com.devexperts.usages.api.* +import com.devexperts.usages.server.Locations +import com.devexperts.usages.server.MemberUsages +import com.devexperts.usages.server.Members +import com.devexperts.usages.server.WithId +import com.devexperts.usages.server.artifacts.ArtifactManager +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction + +object UsagesManager { + + // === PUBLIC API === + + fun geOrCreateMember(member: Member) = transaction { getOrCreateMemberInternal(member) } + + fun getOrCreateLocationId(artifactId: Int, memberId: Int, file: String?, line: Int): Int = transaction { + getOrCreateLocationIdInternal(artifactId, memberId, file, line) + } + + fun addMemberUsage(memberId: Int, locationId: Int, usageKind: UsageKind) = transaction { + addMemberUsageInternal(memberId, locationId, usageKind) + } + + @Synchronized + fun getMemberUsages(memberId: Int): List { + // Caches for members, locations, artifacts + val members = HashMap() + val locations = HashMap() + val artifacts = HashMap() + return transaction { + // todo in the specified artifacts only??? + // Get member associated with the specified memberId + val member = members.computeIfAbsent(memberId, { getMemberInternal(it) }) + MemberUsages.select { MemberUsages.memberId.eq(memberId) }.toList().map { + val usageKind = it[MemberUsages.usageKind] + val location = locations.computeIfAbsent(it[MemberUsages.locationId], { locationId -> + val q = Locations.select { Locations.id.eq(locationId) }.limit(1).first() + Location(artifact = artifacts.computeIfAbsent(q[Locations.artifactId], { ArtifactManager.getArtifactInternal(it).value }), + member = members.computeIfAbsent(q[Locations.memberId], { getMemberInternal(it) }), + file = q[Locations.file], + lineNumber = q[Locations.line] + ) + }) + MemberUsage(member, usageKind, location) + } + } + } + + // === END PUBLIC API === + + private fun getMemberInternal(memberId: Int): Member { + val q = Members.select { Members.id.eq(memberId) }.limit(1).first() + return Member(qualifiedMemberName = q[Members.qualifiedName], + parameterTypes = parseParameterTypes(q[Members.paramTypes]), + type = q[Members.type]) + } + + private fun getOrCreateMemberInternal(member: Member): WithId { + val parameterTypesStr = parameterTypesToString(member.parameterTypes) + var id = Members.slice(Members.id).select { + Members.qualifiedName.eq(member.qualifiedMemberName) and + Members.paramTypes.eq(parameterTypesStr) and + Members.type.eq(member.type) + }.limit(1).firstOrNull()?.get(Members.id) + if (id == null) { + id = Members.insert { + it[Members.qualifiedName] = member.qualifiedMemberName + it[Members.paramTypes] = parameterTypesStr + it[Members.type] = member.type + }[Members.id] + } + return WithId(id, member) + } + + private fun getOrCreateLocationIdInternal(artifactId: Int, memberId: Int, file: String?, line: Int): Int { + val id = Locations.slice(Locations.id).select { + Locations.artifactId.eq(artifactId) and Locations.file.eq(file) and Locations.line.eq(line) + }.limit(1).firstOrNull()?.get(Locations.id) + if (id != null) + return id + return Locations.insert { + it[Locations.artifactId] = artifactId + it[Locations.memberId] = memberId + it[Locations.file] = file + it[Locations.line] = line + }[Locations.id] + } + + private fun addMemberUsageInternal(memberId: Int, locationId: Int, usageKind: UsageKind) { + val exists = MemberUsages.select { + MemberUsages.memberId.eq(memberId) and + MemberUsages.locationId.eq(locationId) and + MemberUsages.usageKind.eq(usageKind) + }.limit(1).count() > 0 + if (!exists) { + MemberUsages.insert { + it[MemberUsages.memberId] = memberId + it[MemberUsages.locationId] = locationId + it[MemberUsages.usageKind] = usageKind + } + } + } + + private fun parameterTypesToString(paramTypes: List): String = paramTypes.joinToString(separator = ",") + private fun parseParameterTypes(parameterTypes: String): List = parameterTypes.split(",") +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/artifacts/ArtifactManager.kt b/server/src/main/kotlin/com/devexperts/usages/server/artifacts/ArtifactManager.kt new file mode 100644 index 0000000..3a3a5eb --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/artifacts/ArtifactManager.kt @@ -0,0 +1,251 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.artifacts + +import com.devexperts.logging.Logging +import com.devexperts.usages.api.Artifact +import com.devexperts.usages.server.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + + +/** + * This component manages artifacts, their dependencies, + * packages and other artifact-related information + */ +object ArtifactManager { + val log: Logging = Logging.getLogging(ArtifactManager::class.java) + + // === PUBLIC API === + + /** + * Store information about the specified artifact in database. + */ + fun storeArtifactInfo(indexerId: String, artifact: Artifact, dependencies: List, + packages: Collection): WithId { + return transaction { + val artifactId = getOrCreateArtifact(artifact).id + addPackagesInternal(artifactId, packages) + addDependenciesInternal(artifactId, dependencies) + setSourceIndexerNameInternal(artifactId, indexerId) + WithId(artifactId, artifact) + } + } + + /** + * Returns ids of artifacts contained the specified package. + */ + fun artifactsWithPackage(pkg: String): List { + // todo approximation by groupId + return transaction { + // Get id associated with the specified package, + // return empty list if this package is not found + val pkgId = getPackageIdInternal(pkg) ?: return@transaction emptyList() + // Get list of ids of the artifacts which contain the specified package + ArtifactPackages.slice(ArtifactPackages.artifactId).select { + ArtifactPackages.packageId.eq(pkgId) + }.toList().map { it[ArtifactPackages.artifactId] } + } + } + + /** + * Return ids of artifacts which have any of the specified artifact as its dependency. + */ + fun artifactIdsWithAnyDependency(artifactIds: List): List { + // todo FIX ME FIX FIX FIX + return transaction { + artifactIds.map { artifactId -> + Dependencies.slice(Dependencies.artifactId).select { + Dependencies.dependencyArtifactId.eq(artifactId) + }.toList().map { it[Dependencies.artifactId] } + }.flatMap { it }.distinct() + } + } + + /** + * Returns the [Artifact] associated with the specified id. + */ + fun getArtifact(id: Int): WithId = transaction { + getArtifactInternal(id) + } + + /** + * Returns the [Artifact] associated with the specified id. + * Should be invoked inside [transaction] + */ + fun getArtifactInternal(id: Int): WithId { + val res = Artifacts.select { + Artifacts.id.eq(id) + }.limit(1).first() + val artifact = Artifact(groupId = res[Artifacts.groupId], + artifactId = res[Artifacts.artifactId], + version = res[Artifacts.version], + type = res[Artifacts.type], + classifier = res[Artifacts.classifier]) + return WithId(id, artifact) + } + + fun getSourceIndexerName(artifactId: Int): String = transaction { + ArtifactSources.slice(ArtifactSources.indexerId).select { + ArtifactSources.artifactId.eq(artifactId) + }.limit(1).first()[ArtifactSources.indexerId] + } + + fun isAnalyzed(artifactId: Int) = transaction { + ArtifactStatus.slice(ArtifactStatus.analyzed).select { + ArtifactStatus.artifactId.eq(artifactId) + }.limit(1).first()[ArtifactStatus.analyzed] + } + + fun markAnalyzed(artifactId: Int) = transaction { + ArtifactStatus.update({ ArtifactStatus.artifactId.eq(artifactId) }) { + it[ArtifactStatus.analyzed] = true + } + } + + // === END PUBLIC API === + + + /** + * Inserts the specified artifact to the database if needed and + * returns an id associated with the added artifact. + * Should be invoked under [transaction]. + */ + private fun getOrCreateArtifact(artifact: Artifact): WithId { + var id = getArtifactIdInternal(artifact) + if (id == null) { + id = Artifacts.insert { + it[artifactId] = artifact.artifactId + it[groupId] = artifact.groupId + it[version] = artifact.version + it[type] = artifact.type + it[classifier] = artifact.classifier + }[Artifacts.id] + addEmptyArtifactStatusInternal(id) + } + return WithId(id, artifact) + } + + + /** + * TODO + */ + private fun addEmptyArtifactStatusInternal(artifactId: Int) { + ArtifactStatus.insert { + it[ArtifactStatus.artifactId] = artifactId + it[ArtifactStatus.analyzed] = false + it[ArtifactStatus.hasPackages] = false + } + } + + /** + * TODO + */ + private fun setSourceIndexerNameInternal(artifactId: Int, indexerId: String) { + val exist = ArtifactSources.select { ArtifactSources.artifactId.eq(artifactId) } + .limit(1).count() > 0 + if (!exist) { + ArtifactSources.insert { + it[ArtifactSources.artifactId] = artifactId + it[ArtifactSources.indexerId] = indexerId + } + } + } + + /** + * Add information of packages in the artifact with the specified id. + * Should be invoked under [transaction]. + */ + private fun addPackagesInternal(artifactId: Int, packages: Collection) { + val packageIds = packages.map { pkg -> addPackageIfNeededInternal(pkg) } + val alreadyStoredPackageIds = ArtifactPackages.slice(ArtifactPackages.packageId) + .select { + ArtifactPackages.artifactId.eq(artifactId) + }.map { it[ArtifactPackages.packageId] } + ArtifactPackages.batchInsert((HashSet(packageIds) - alreadyStoredPackageIds)) { pkgId -> + this[ArtifactPackages.artifactId] = artifactId + this[ArtifactPackages.packageId] = pkgId + } + } + + /** + * Add information of the specified artifact dependencies. + * Should be invoked under [transaction]. + */ + private fun addDependenciesInternal(artifactId: Int, dependencies: List) { + transaction { + // Get ids of the specified dependencies + val dependencyIds = dependencies.map { getOrCreateArtifact(it).id } + // Get already added dependencies + val curDependencyIds = HashSet(getDependenciesInternal(artifactId)) + // This set contains ids of dependencies + Dependencies.batchInsert(dependencyIds - curDependencyIds) { depId -> + this[Dependencies.artifactId] = artifactId + this[Dependencies.dependencyArtifactId] = depId + } + } + } + + /** + * Returns ids of specified artifact dependencies. + * Should be invoked under [transaction]. + */ + private fun getDependenciesInternal(artifactId: Int): List { + val q = Dependencies.slice(Dependencies.dependencyArtifactId).select { + Dependencies.artifactId.eq(artifactId) + } + return q.toList().map { it[Dependencies.dependencyArtifactId] } + } + + /** + * Get id of the specified [Artifact]. + * Should be invoked under [transaction]. + */ + private fun getArtifactIdInternal(artifact: Artifact): Int? { + val q = Artifacts.slice(Artifacts.id).select { + Artifacts.artifactId.eq(artifact.artifactId) and + Artifacts.groupId.eq(artifact.groupId) and + Artifacts.version.eq(artifact.version) and + Artifacts.type.eq(artifact.type) and + Artifacts.classifier.eq(artifact.classifier) + }.limit(1) + return q.firstOrNull()?.get(Artifacts.id) + } + + /** + * Get id of the specified package. + * Should be invoked under [transaction]. + */ + private fun getPackageIdInternal(pkg: String) = Packages.slice(Packages.id) + .select { Packages.pkg.eq(pkg) }.limit(1) + .firstOrNull()?.get(Packages.id) + + /** + * Inserts the specified package to the database if needed and + * returns an id associated with the added package. + * Should be invoked under [transaction]. + */ + private fun addPackageIfNeededInternal(pkg: String): Int { + val pkgId = getPackageIdInternal(pkg) + if (pkgId != null) + return pkgId + return Packages.insert { + it[Packages.pkg] = pkg + }[Packages.id] + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/config/Configuration.kt b/server/src/main/kotlin/com/devexperts/usages/server/config/Configuration.kt new file mode 100644 index 0000000..ce0df19 --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/config/Configuration.kt @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.config + +import org.aeonbits.owner.Config +import org.aeonbits.owner.ConfigFactory +import java.io.File + +@Config.Sources("classpath:usages.properties") +interface PropertiesConfiguration : Config { + @Config.Key("usages.workDir") + @Config.DefaultValue("~/.usages") + fun workDir(): String +} + +private val configuration = ConfigFactory.create(PropertiesConfiguration::class.java, System.getProperties()) + +object Configuration { + val workDir = resolvePath(configuration.workDir()) + val settingsFile = workDirFile("settings.xml") + val dbFile = workDirFile("usages_db") + + private fun resolvePath(file: String): String { + var f = file + if (f.startsWith('~')) + f = System.getProperty("user.home") + f.substring(1) + return f + } +} + +private fun workDirFile(file: String) = Configuration.workDir + File.separator + file + + + + + diff --git a/server/src/main/kotlin/com/devexperts/usages/server/config/Settings.kt b/server/src/main/kotlin/com/devexperts/usages/server/config/Settings.kt new file mode 100644 index 0000000..e679cd3 --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/config/Settings.kt @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.config + +import com.devexperts.util.TimePeriod +import org.simpleframework.xml.Element +import org.simpleframework.xml.ElementList +import org.simpleframework.xml.Path +import org.simpleframework.xml.Root +import org.simpleframework.xml.convert.AnnotationStrategy +import org.simpleframework.xml.convert.Convert +import org.simpleframework.xml.convert.Converter +import org.simpleframework.xml.core.Persister +import org.simpleframework.xml.stream.InputNode +import org.simpleframework.xml.stream.OutputNode +import java.io.File + +private const val ID_TAG = "id" +private const val URL_TAG = "url" +private const val TYPE_TAG = "type" +private const val USER_TAG = "user" +private const val PASSWORD_TAG = "password" +private const val SCAN_TIME_PERIOD_TAG = "scanTimePeriod" + +@Root(name = "settings") +class Settings { + @field:Path("repositories") + @field:ElementList(inline = true, type = RepositorySetting::class, empty = false) + lateinit var repositorySettings: List + + @field:Path("artifactTypes") + @field:ElementList(inline = true, entry = "type", empty = false) + lateinit var artifactTypes: List +} + +@Root(name = "repository") +class RepositorySetting { + @field:Element(name = ID_TAG) + lateinit var id: String + + @field:Element(name = URL_TAG) + lateinit var url: String + + @field:Element(name = TYPE_TAG) + @field:Convert(RepositoryTypeConverter::class) + lateinit var type: RepositoryType + + @field:Element(name = USER_TAG, required = false) + var user: String? = null + + @field:Element(name = PASSWORD_TAG, required = false) + var password: String? = null + + @field:Element(name = SCAN_TIME_PERIOD_TAG) + @field:Convert(TimePeriodConverter::class) + var scanTimePeriod: TimePeriod = TimePeriod.valueOf("15m") +} + +enum class RepositoryType(val typeName: String) { + NEXUS("nexus"), + ARTIFACTORY("artifactory"); + + override fun toString(): String = typeName +} + +private class RepositoryTypeConverter : Converter { + override fun read(node: InputNode): RepositoryType { + return RepositoryType.values().first { it.typeName == node.value } + } + + override fun write(node: OutputNode, value: RepositoryType) { + node.name = TYPE_TAG + node.value = value.typeName + } +} + +private class TimePeriodConverter : Converter { + override fun read(node: InputNode): TimePeriod { + return TimePeriod.valueOf(node.value) + } + + override fun write(node: OutputNode, value: TimePeriod) { + node.name = SCAN_TIME_PERIOD_TAG + node.value = value.toString() + } +} + +fun readSettings(): Settings { + val serializer = Persister(AnnotationStrategy()) + return serializer.read(Settings::class.java, File(Configuration.settingsFile)) +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/indexer/IndexerCli.kt b/server/src/main/kotlin/com/devexperts/usages/server/indexer/IndexerCli.kt new file mode 100644 index 0000000..a144b27 --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/indexer/IndexerCli.kt @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.indexer + +import com.devexperts.logging.Logging +import com.devexperts.usages.api.Artifact +import com.devexperts.usages.server.initDatabase +import com.devexperts.usages.server.config.Configuration +import com.devexperts.usages.server.config.readSettings +import kotlinx.coroutines.experimental.CommonPool +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.runBlocking + +val cliLog: Logging = Logging.getLogging("IndexerCli") + +fun main(args: Array) { + // Create indexers + val settings = readSettings() + val indexers = settings.createIndexers() + // Init database + initDatabase(Configuration.dbFile) + // Parse operation to execute + when (args[0]) { + "scan" -> scan(indexers) + "download" -> download(indexers, args[1]) + } +} + +fun scan(indexers: List) = runBlocking { + val jobs = arrayListOf() + indexers.forEach { indexer -> + try { + jobs += launch(CommonPool) { + indexer.scan() + } + } catch (e: Throwable) { + cliLog.error("Error during ${indexer.id} repository indexing", e) + } + } + jobs.forEach { it.join() } +} + +fun download(indexers: List, artifactDesc: String) { + val props = artifactDesc.split(":") + val artifact = when { + props.size == 3 -> // groupId:artifactId:version + Artifact(groupId = props[0], artifactId = props[1], version = props[2], + type = null, classifier = null) + props.size == 5 -> // groupId:artifactId:type:classifier:version + Artifact(groupId = props[0], artifactId = props[1], + type = props[2], classifier = props[3], version = props[4]) + else -> throw IllegalArgumentException("Invalid artifact description: " + artifactDesc) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/indexer/MavenIndexer.kt b/server/src/main/kotlin/com/devexperts/usages/server/indexer/MavenIndexer.kt new file mode 100644 index 0000000..364cc39 --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/indexer/MavenIndexer.kt @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.indexer + +import com.devexperts.logging.Logging +import com.devexperts.usages.api.Artifact +import com.devexperts.usages.server.artifacts.ArtifactManager +import com.devexperts.usages.server.config.RepositorySetting +import com.devexperts.usages.server.config.RepositoryType +import com.devexperts.usages.server.config.Settings +import com.devexperts.util.TimePeriod +import java.io.File + +abstract class MavenIndexer( + repositorySetting: RepositorySetting, // delay between repository indexing runs. + val supportedArtifactTypes: List, + val id: String = repositorySetting.id, + val url: String = repositorySetting.url, + val user: String? = repositorySetting.user, + val password: String? = repositorySetting.password, + val scanDelay: TimePeriod = repositorySetting.scanTimePeriod +) { + + protected abstract val log: Logging + + /** + * Scans maven repository structure, + * stores information about artifacts and their transitive dependencies. + */ + abstract fun scan() + + /** + * Downloads the specified artifact from the repository. + * Returns null if the artifact has not been downloaded. + */ + abstract fun downloadArtifact(artifact: Artifact): File? + + /** + * Store information about the artifact into the [ArtifactManager] + */ + protected fun storeArtifactInfo(artifact: Artifact, dependencies: List, packages: Collection) { + ArtifactManager.storeArtifactInfo(id, artifact, dependencies, packages) + log.trace("Store information for $artifact, dependencies=$dependencies, packages=$packages") + } +} + +/** + * Creates [MavenIndexer] according to this settings + */ +fun RepositorySetting.createIndexer(supportedArtifactTypes: List): MavenIndexer { + when (type) { + RepositoryType.NEXUS -> return NexusMavenIndexer( + repositorySetting = this, + supportedArtifacts = supportedArtifactTypes + ) + else -> throw IllegalStateException("Unsupported repository type: $type") + } +} + +/** + * Creates a list of [MavenIndexer]s according to this settings + */ +fun Settings.createIndexers() = repositorySettings.map { + it.createIndexer(artifactTypes) +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/devexperts/usages/server/indexer/NexusMavenIndexer.kt b/server/src/main/kotlin/com/devexperts/usages/server/indexer/NexusMavenIndexer.kt new file mode 100644 index 0000000..b7bd2c1 --- /dev/null +++ b/server/src/main/kotlin/com/devexperts/usages/server/indexer/NexusMavenIndexer.kt @@ -0,0 +1,275 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.indexer + +import com.devexperts.logging.Logging +import com.devexperts.usages.api.Artifact +import com.devexperts.usages.server.analyzer.toClassName +import com.devexperts.usages.server.config.Configuration +import com.devexperts.usages.server.config.RepositorySetting +import org.apache.maven.index.ArtifactInfo +import org.apache.maven.index.Indexer +import org.apache.maven.index.context.IndexCreator +import org.apache.maven.index.context.IndexUtils +import org.apache.maven.index.updater.IndexUpdateRequest +import org.apache.maven.index.updater.IndexUpdater +import org.apache.maven.index.updater.WagonHelper +import org.apache.maven.repository.internal.MavenRepositorySystemUtils +import org.apache.maven.wagon.authentication.AuthenticationInfo +import org.apache.maven.wagon.events.TransferEvent +import org.apache.maven.wagon.observers.AbstractTransferListener +import org.apache.maven.wagon.providers.http.LightweightHttpWagon +import org.apache.maven.wagon.providers.http.LightweightHttpWagonAuthenticator +import org.codehaus.plexus.DefaultContainerConfiguration +import org.codehaus.plexus.DefaultPlexusContainer +import org.codehaus.plexus.PlexusConstants +import org.codehaus.plexus.PlexusContainer +import org.eclipse.aether.AbstractRepositoryListener +import org.eclipse.aether.RepositoryEvent +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory +import org.eclipse.aether.impl.DefaultServiceLocator +import org.eclipse.aether.repository.LocalRepository +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.resolution.ArtifactDescriptorRequest +import org.eclipse.aether.resolution.ArtifactRequest +import org.eclipse.aether.resolution.ArtifactResolutionException +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory +import org.eclipse.aether.spi.connector.transport.TransporterFactory +import org.eclipse.aether.transport.file.FileTransporterFactory +import org.eclipse.aether.transport.http.HttpTransporterFactory +import org.eclipse.aether.util.repository.AuthenticationBuilder +import java.io.File +import java.util.stream.Collectors + +class NexusMavenIndexer(repositorySetting: RepositorySetting, + supportedArtifacts: List) + : MavenIndexer(repositorySetting, supportedArtifacts) { + override val log: Logging = Logging.getLogging(NexusMavenIndexer::class.java) + + private val cacheDir = File(Configuration.workDir + File.separator + "cache" + File.separator + id) + private val centralIndexDir = File(Configuration.workDir + File.separator + "central-index" + File.separator + id) + private val localRepositoryDir = File(Configuration.workDir + File.separator + "local-repository" + File.separator + id) + + private val authenticationInfo: AuthenticationInfo = AuthenticationInfo() + private val remoteRepository: RemoteRepository + private val repositorySystem: RepositorySystem = newRepositorySystem() + private val repositorySystemSession: RepositorySystemSession = newRepositorySystemSession(repositorySystem) + private val wagon: LightweightHttpWagon + + private val plexusContainer: PlexusContainer + private val indexer: Indexer + private val indexUpdater: IndexUpdater + + init { + val remoteRepoBuilder = RemoteRepository.Builder(id, "default", url) + if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) { + authenticationInfo.userName = user + authenticationInfo.password = password + remoteRepoBuilder.setAuthentication(AuthenticationBuilder() + .addUsername(user).addPassword(password).build()) + } + remoteRepository = remoteRepoBuilder.build() + wagon = LightweightHttpWagon() + wagon.authenticator = LightweightHttpWagonAuthenticator() + val config = DefaultContainerConfiguration() + config.classPathScanning = PlexusConstants.SCANNING_INDEX + plexusContainer = DefaultPlexusContainer(config) + indexer = plexusContainer.lookup(Indexer::class.java) + indexUpdater = plexusContainer.lookup(IndexUpdater::class.java) + } + + override fun scan() { + log.info("[$id] Start repository indexing") + val indexers = listOf("min", "jarContent", "maven-plugin").stream() + .map({ roleHint -> plexusContainer.lookup(IndexCreator::class.java, roleHint) }) + .collect(Collectors.toList()) + val indexingContext = indexer.createIndexingContext(id, id, cacheDir, centralIndexDir, url, + null, false, true, indexers) + try { + val resourceFetcher = WagonHelper.WagonFetcher(wagon, ResourceFetcherListener(), authenticationInfo, null) + val updateResult = indexUpdater.fetchAndUpdateIndex(IndexUpdateRequest(indexingContext, resourceFetcher)) + if (updateResult.timestamp != null && updateResult.timestamp < indexingContext.timestamp) { + log.info("[$id] Everything is up-to-date, scanning completed") + return + } + log.info("[$id] Updating artifacts information...") + val indexSearcher = indexingContext.acquireIndexSearcher() + try { + indexSearcher.use { + repeat(it.maxDoc()) { i -> + if (it.indexReader.isDeleted(i)) + return@repeat + // Construct ArtifactInfo for current doc + val doc = it.indexReader.document(i) + val artifactInfo = IndexUtils.constructArtifactInfo(doc, indexingContext) ?: return@repeat + // Process artifacts with specified extensions only + if ("pom" != artifactInfo.fextension && !supportedArtifactTypes.contains(artifactInfo.fextension)) + return@repeat + // Do not process sources and javadocs + if (artifactInfo.classifier == "sources" || artifactInfo.classifier == "javadoc") + return@repeat + // Get list of classes + val classes = doc.getFieldable("c")?.stringValue() + try { + processArtifact(artifactInfo, classes) + } catch (e: Throwable) { + log.warn("[$id] Error while processing artifact $artifactInfo", e) + } + } + } + } finally { + indexingContext.releaseIndexSearcher(indexSearcher) + } + log.info("[$id] Repository indexing has completed successfully") + } catch (e: Throwable) { + log.warn("[$id] Repository indexing has failed with error", e) + } finally { + try { + indexingContext.close(false) + } catch (e: Throwable) { + // Ignore + log.warn("[$id] Error during indexing context closing", e) + } + } + } + + private fun processArtifact(artifactInfo: ArtifactInfo, classes: String?) { + val artifact = artifactInfoToArtifact(artifactInfo) + // Retrieve packages + val packages = HashSet() + classes?.split(Regex("\\s"))?.forEach { internalClassName -> + // internal className starts with '/', remove it + val className = toClassName(internalClassName.substring(1)) + // Add package of this class to the set if it is not empty + val lastDotIndex = className.lastIndexOf('.') + if (lastDotIndex > 0) { + val pkg = className.substring(0, lastDotIndex) + packages.add(pkg) + } + } + // Retrieve dependencies + val request = ArtifactDescriptorRequest() + .setArtifact(artifactToAetherArtifact(artifact)) + .addRepository(remoteRepository) + val dependencies = repositorySystem.readArtifactDescriptor(repositorySystemSession, request).dependencies + .map { it.artifact } + .filter { /*"pom" == it.extension || */supportedArtifactTypes.contains(it.extension) } + .map { aetherArtifactToArtifact(it) } + // Store artifact info + storeArtifactInfo(artifact, dependencies, packages) + } + + override fun downloadArtifact(artifact: Artifact): File? { + try { + // Create request in order to get aether's artifact + val request = ArtifactRequest() + .setArtifact(artifactToAetherArtifact(artifact)) + .addRepository(remoteRepository) + // Perform the request, + val aetherArtifact = repositorySystem + .resolveArtifact(repositorySystemSession, request).artifact + // Get [File] or return null if artifact has not been resolved + val file = aetherArtifact?.file + if (file == null) { + log.warn("Downloading $artifact failed, artifact is not resolved") + return null + } + // todo fill package information here if needed + return file + } catch (e: ArtifactResolutionException) { + log.warn("Downloading $artifact failed, error during resolution", e) + return null + } + } + + private fun artifactInfoToArtifact(artifactInfo: ArtifactInfo) = Artifact( + groupId = artifactInfo.groupId, + artifactId = artifactInfo.artifactId, + classifier = artifactInfo.classifier, + type = artifactInfo.fextension, + version = artifactInfo.version + ) + + private fun artifactToAetherArtifact(artifact: Artifact): org.eclipse.aether.artifact.Artifact = DefaultArtifact( + artifact.groupId, artifact.artifactId, artifact.classifier, artifact.type, artifact.version + ) + + private fun aetherArtifactToArtifact(aetherArtifact: org.eclipse.aether.artifact.Artifact) = Artifact( + groupId = aetherArtifact.groupId, + artifactId = aetherArtifact.artifactId, + classifier = aetherArtifact.classifier, + type = aetherArtifact.extension, + version = aetherArtifact.version + ) + + private fun newRepositorySystemSession(system: RepositorySystem): RepositorySystemSession { + val session = MavenRepositorySystemUtils.newSession() + val localRepo = LocalRepository(localRepositoryDir) + session.localRepositoryManager = system.newLocalRepositoryManager(session, localRepo) + session.repositoryListener = LoggedRepositoryListener() + return session + } + + private fun newRepositorySystem(): RepositorySystem { + val locator = MavenRepositorySystemUtils.newServiceLocator() + locator.addService(RepositoryConnectorFactory::class.java, BasicRepositoryConnectorFactory::class.java) + locator.addService(TransporterFactory::class.java, FileTransporterFactory::class.java) + locator.addService(TransporterFactory::class.java, HttpTransporterFactory::class.java) + locator.setErrorHandler(object : DefaultServiceLocator.ErrorHandler() { + override fun serviceCreationFailed(type: Class<*>, impl: Class<*>, e: Throwable) { + log.warn("Error while creating new repository system", e) + } + }) + return locator.getService(RepositorySystem::class.java) + } + + private inner class LoggedRepositoryListener : AbstractRepositoryListener() { + private val log: Logging = Logging.getLogging(LoggedRepositoryListener::class.java) + + override fun artifactDescriptorInvalid(event: RepositoryEvent) { + log.warn("Invalid artifact descriptor, ${this@NexusMavenIndexer.id} indexing: $event") + } + + override fun metadataInvalid(event: RepositoryEvent) { + log.warn("Invalid metadata, ${this@NexusMavenIndexer.id} indexing: $event") + } + + override fun artifactDescriptorMissing(event: RepositoryEvent) { + log.warn("Invalid artifact descriptor, ${this@NexusMavenIndexer.id} indexing: $event") + } + } + + private inner class ResourceFetcherListener : AbstractTransferListener() { + private val log: Logging = Logging.getLogging(ResourceFetcherListener::class.java) + + override fun transferStarted(event: TransferEvent) { + log.debug("[${this@NexusMavenIndexer.id}] Downloading ${event.resource.name}") + } + + override fun transferCompleted(event: TransferEvent) { + log.debug("[${this@NexusMavenIndexer.id}] Downloaded ${event.resource.name}") + } + + override fun transferError(event: TransferEvent) { + log.debug("[${this@NexusMavenIndexer.id}] Downloading failed ${event.resource.name}") + } + } +} \ No newline at end of file diff --git a/server/src/test/kotlin/com/devexperts/usages/server/TestElements.kt b/server/src/test/kotlin/com/devexperts/usages/server/TestElements.kt new file mode 100644 index 0000000..ae6d8f9 --- /dev/null +++ b/server/src/test/kotlin/com/devexperts/usages/server/TestElements.kt @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server + +import com.devexperts.usages.api.Artifact +import com.devexperts.usages.api.Member +import com.devexperts.usages.api.MemberType + +val artifact1 = Artifact( + groupId = "com.devexperts.usages", + artifactId = "usages", + version = "2017", + type = null, + classifier = null +) +val artifact2 = Artifact( + groupId = "com.devexperts.qd", + artifactId = "dxlib", + version = "3.154", + type = null, + classifier = null +) +val artifact3 = Artifact( + groupId = "com.devexperts.qd", + artifactId = "qd-core", + version = "3.155", + type = null, + classifier = null +) + + +val pkg1 = "com" +val pkg2 = "com.devexperts" +val pkg3 = "com.devexperts.usages" +val pkg4 = "com.devexperts.util" + +val pkg123 = arrayListOf(pkg1, pkg2, pkg3) + + +val member1 = Member("com.devexperts.usages.server.ServerKt", emptyList(), MemberType.CLASS) +val member2 = Member("com.devexperts.usages.server.ServerKt#fff", + listOf("java.lang.String", "com.devexperts.usages.MyClass"), MemberType.METHOD) diff --git a/server/src/test/kotlin/com/devexperts/usages/server/analyzer/UsagesManagerTest.kt b/server/src/test/kotlin/com/devexperts/usages/server/analyzer/UsagesManagerTest.kt new file mode 100644 index 0000000..2e87f29 --- /dev/null +++ b/server/src/test/kotlin/com/devexperts/usages/server/analyzer/UsagesManagerTest.kt @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.analyzer + +import com.devexperts.usages.api.UsageKind +import com.devexperts.usages.server.* +import com.devexperts.usages.server.artifacts.ArtifactManager +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class UsagesManagerTest { + private var id1: Int = -1 + private var id2: Int = -1 + private var id3: Int = -1 + + @Before + fun setUp() { + initInMemoryDatabase() + id1 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact1, + dependencies = listOf(artifact2), packages = pkg123).id + id2 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact2, + dependencies = emptyList(), packages = listOf(pkg4)).id + id3 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact3, + dependencies = emptyList(), packages = listOf(pkg2)).id + } + + @After + fun tearDown() = dropDatabase() + + @Test + fun testAddSameMembers() { + val id1 = UsagesManager.geOrCreateMember(member1).id + val id2 = UsagesManager.geOrCreateMember(member1).id + assertEquals(id1, id2) + } + + @Test + fun testAddSameLocations() { + val memberId1 = UsagesManager.geOrCreateMember(member1).id + val id1 = UsagesManager.getOrCreateLocationId(id1, memberId1, null, -1) + val id2 = UsagesManager.getOrCreateLocationId(id1, memberId1, null, -1) + assertEquals(id1, id2) + } + + @Test + fun testAddMemberUsagesWithDifferentKind() { + val memberId1 = UsagesManager.geOrCreateMember(member1).id + val location1 = UsagesManager.getOrCreateLocationId(id1, memberId1, null, -1) + UsagesManager.addMemberUsage(memberId1, location1, UsageKind.UNCLASSIFIED) + UsagesManager.addMemberUsage(memberId1, location1, UsageKind.CLASS_DECLARATION) + } + + @Test + fun testAddSameMemberUsages() { + val memberId1 = UsagesManager.geOrCreateMember(member1).id + val location1 = UsagesManager.getOrCreateLocationId(id1, memberId1, null, -1) + UsagesManager.addMemberUsage(memberId1, location1, UsageKind.UNCLASSIFIED) + UsagesManager.addMemberUsage(memberId1, location1, UsageKind.UNCLASSIFIED) + } + + @Test + fun testGetMemberUsages() { + val memberId1 = UsagesManager.geOrCreateMember(member1).id + val memberId2 = UsagesManager.geOrCreateMember(member2).id + val location1 = UsagesManager.getOrCreateLocationId(id1, memberId1, null, -1) + val location2 = UsagesManager.getOrCreateLocationId(id2, memberId1, null, -1) + val location3 = UsagesManager.getOrCreateLocationId(id1, memberId2, null, -1) + UsagesManager.addMemberUsage(memberId1, location1, UsageKind.CAST) + UsagesManager.addMemberUsage(memberId1, location2, UsageKind.CAST) + UsagesManager.addMemberUsage(memberId1, location3, UsageKind.OVERRIDE) + UsagesManager.addMemberUsage(memberId2, location1, UsageKind.UNCLASSIFIED) + UsagesManager.addMemberUsage(memberId2, location3, UsageKind.UNCLASSIFIED) + UsagesManager.addMemberUsage(memberId1, location1, UsageKind.UNCLASSIFIED) + val usages = UsagesManager.getMemberUsages(memberId1) + assertEquals(4, usages.size) + } +} \ No newline at end of file diff --git a/server/src/test/kotlin/com/devexperts/usages/server/artifacts/ArtifactManagerTest.kt b/server/src/test/kotlin/com/devexperts/usages/server/artifacts/ArtifactManagerTest.kt new file mode 100644 index 0000000..dc15114 --- /dev/null +++ b/server/src/test/kotlin/com/devexperts/usages/server/artifacts/ArtifactManagerTest.kt @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2017 Devexperts LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + */ +package com.devexperts.usages.server.artifacts + +import com.devexperts.usages.server.* +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class ArtifactManagerTest { + @Before + fun setUp() = initInMemoryDatabase() + + @After + fun tearDown() = dropDatabase() + + @Test + fun testStoreArtifactInfo() { + val id1 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact1, + dependencies = listOf(artifact2), packages = pkg123).id + val id2 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact2, + dependencies = emptyList(), packages = listOf(pkg4)).id + val toAnalyze = ArtifactManager.artifactIdsWithAnyDependency(listOf(id2)) + assertEquals(1, toAnalyze.size) + assertTrue(toAnalyze.contains(id1)) + assertFalse(ArtifactManager.isAnalyzed(id1)) + assertEquals("jrc", ArtifactManager.getSourceIndexerName(id1)) + } + + @Test + fun testAddSameArtifact() { + val id1 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact1, + dependencies = listOf(artifact2), packages = pkg123).id + val id2 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact1, + dependencies = listOf(artifact2), packages = pkg123).id + assertEquals(id1, id2) + } + + @Test + fun testArtifactsWithPackage() { + val id1 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact1, + dependencies = listOf(artifact2), packages = pkg123).id + val id2 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact2, + dependencies = emptyList(), packages = listOf(pkg4)).id + val id3 = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact3, + dependencies = emptyList(), packages = listOf(pkg2)).id + val res = ArtifactManager.artifactsWithPackage(pkg2) + assertEquals(2, res.size) + assertTrue(res.contains(id1)) + assertFalse(res.contains(id2)) + assertTrue(res.contains(id3)) + } + + @Test + fun testGetArtifact() { + val id = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact1, + dependencies = listOf(artifact2), packages = pkg123).id + val a = ArtifactManager.getArtifact(id).value + assertEquals(artifact1, a) + } + + @Test + fun testMarkAnalyzed() { + val id = ArtifactManager.storeArtifactInfo(indexerId = "jrc", artifact = artifact1, + dependencies = listOf(artifact2), packages = pkg123).id + ArtifactManager.markAnalyzed(id) + assertTrue(ArtifactManager.isAnalyzed(id)) + } +} \ No newline at end of file