Initial commit Initial version of repository-usage plugin, tested against H2 and PostgreSQL. Currently able to scan submodule updates and manifest files in the root tree. Change-Id: I4988b4215211817d2af8ca8b64c9d013a72fce95
diff --git a/.buckconfig b/.buckconfig new file mode 100644 index 0000000..dd4f476 --- /dev/null +++ b/.buckconfig
@@ -0,0 +1,15 @@ +[alias] + repository-usage = //:repository-usage + plugin = //:repository-usage + src = //:repository-usage-sources + +[java] + src_roots = java, resources + +[project] + ignore = .git + +[cache] + mode = dir + dir = buck-out/cache +
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..525de60 --- /dev/null +++ b/.gitignore
@@ -0,0 +1,3 @@ +.project +.classpath +buck-out
diff --git a/BUCK b/BUCK new file mode 100644 index 0000000..d894755 --- /dev/null +++ b/BUCK
@@ -0,0 +1,66 @@ +include_defs('//bucklets/gerrit_plugin.bucklet') +include_defs('//lib/maven.defs') + +DEPS = [ + ':digester3', + ':beanutils', + ':logging', +] +PROVIDED_DEPS = [ + '//lib/commons:dbcp', + '//lib:gson', +] +TEST_DEPS = GERRIT_PLUGIN_API + [ + ':repository-usage__plugin', + '//lib:junit', + '//lib:truth', +] + +gerrit_plugin( + name = 'repository-usage', + srcs = glob(['src/main/java/**/*.java']), + resources = glob(['src/main/resources/**/*']), + manifest_entries = [ + 'Gerrit-PluginName: repository-usage', + 'Gerrit-Module: com.googlesource.gerrit.plugins.repositoryuse.Module', + ], + deps = DEPS, + provided_deps = PROVIDED_DEPS, +) + +java_library( + name = 'classpath', + deps = [':repository-usage__plugin'], +) + +java_test( + name = 'repository-usage_tests', + srcs = glob(['src/test/java/**/*.java']), + labels = ['repository-usage'], + source_under_test = [':repository-usage__plugin'], + deps = TEST_DEPS, +) + +maven_jar( + name = 'digester3', + id = 'org.apache.commons:commons-digester3:3.2', + sha1 = 'c3f68c5ff25ec5204470fd8fdf4cb8feff5e8a79', + license = 'Apache2.0', + exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], +) + +maven_jar( + name = 'beanutils', + id = 'commons-beanutils:commons-beanutils:1.8.3', + sha1 = '686ef3410bcf4ab8ce7fd0b899e832aaba5facf7', + license = 'Apache2.0', + exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], +) + +maven_jar( + name = 'logging', + id = 'commons-logging:commons-logging:1.1.1', + sha1 = '5043bfebc3db072ed80fbd362e7caf00e885d8ae', + license = 'Apache2.0', +) +
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE
@@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.
diff --git a/lib/BUCK b/lib/BUCK new file mode 100644 index 0000000..f501067 --- /dev/null +++ b/lib/BUCK
@@ -0,0 +1,27 @@ +include_defs('//bucklets/maven_jar.bucklet') + +maven_jar( + name = 'junit', + id = 'junit:junit:4.10', + sha1 = 'e4f1766ce7404a08f45d859fb9c226fc9e41a861', + license = 'DO_NOT_DISTRIBUTE', + deps = [':hamcrest-core'], +) + +maven_jar( + name = 'hamcrest-core', + id = 'org.hamcrest:hamcrest-core:1.3', + sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0', + license = 'DO_NOT_DISTRIBUTE', + visibility = ['//lib:junit'], +) + +maven_jar( + name = 'truth', + id = 'com.google.truth:truth:0.26', + sha1 = 'b5802815625d82f39c33219299771f3d64301b06', + license = 'DO_NOT_DISTRIBUTE', + deps = [ + ':junit', + ], +)
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK new file mode 100644 index 0000000..96d016a --- /dev/null +++ b/lib/gerrit/BUCK
@@ -0,0 +1,13 @@ +include_defs('//bucklets/maven_jar.bucklet') + +VER = '2.11' +REPO = MAVEN_CENTRAL + +maven_jar( + name = 'plugin-api', + id = 'com.google.gerrit:gerrit-plugin-api:' + VER, + sha1 = 'be80ff991f7b9f8669b7a2a399003ec1ae69ed31', + license = 'Apache2.0', + attach_source = False, + repository = REPO, +)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Config.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Config.java new file mode 100644 index 0000000..3722cd8 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Config.java
@@ -0,0 +1,108 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.repositoryuse; + +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.Inject; + +public class Config { + public enum Database { + H2, POSTGRESQL + } + + @Inject + private static PluginConfigFactory cfg; + + @Inject + @PluginName + private static String pluginName; + + @Inject + private static SitePaths sitePaths; + + private static boolean configParsed = false; + private static boolean refreshAllSubmodules; + private static boolean parseManifests; + private static Database databaseType; + private static String database; + private static String databaseHost; + private static String databaseUser; + private static String databasePassword; + + private static void readConfig() { + PluginConfig pc = cfg.getFromGerritConfig(pluginName); + refreshAllSubmodules = pc.getBoolean("refreshAllSubmodules", false); + parseManifests = pc.getBoolean("parseManifests", true); + databaseType = pc.getEnum("databaseType", Database.H2); + database = pc.getString("database", + sitePaths.site_path.resolve("db/UsageDB").toString()); + databaseHost = pc.getString("databaseHost", ""); + databaseUser = pc.getString("databaseUser", ""); + databasePassword = pc.getString("databasePassword", ""); + configParsed = true; + } + + public static boolean refreshAllSubmodules() { + if (!configParsed) { + readConfig(); + } + return refreshAllSubmodules; + } + + public static boolean parseManifests() { + if (!configParsed) { + readConfig(); + } + return parseManifests; + } + + public static Database getDatabaseType() { + if (!configParsed) { + readConfig(); + } + return databaseType; + } + + public static String getDatabase() { + if (!configParsed) { + readConfig(); + } + return database; + } + + public static String getDatabaseHost() { + if (!configParsed) { + readConfig(); + } + return databaseHost; + } + + public static String getDatabaseUser() { + if (!configParsed) { + readConfig(); + } + return databaseUser; + } + + public static String getDatabasePassword() { + if (!configParsed) { + readConfig(); + } + return databasePassword; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ManifestParser.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ManifestParser.java new file mode 100644 index 0000000..acaa3ee --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ManifestParser.java
@@ -0,0 +1,173 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.repositoryuse; + +import org.apache.commons.digester3.Digester; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class ManifestParser { + private static final Logger log = + LoggerFactory.getLogger(ManifestParser.class); + private HashMap<String, String> remotes; + private Project defaultProject; + private ArrayList<Project> projects; + + public ManifestParser() { + remotes = new HashMap<>(); + defaultProject = new Project(null, null, null); + projects = new ArrayList<>(); + } + + public Map<String, String> parseManifest(byte[] contents) { + Digester digester = new Digester(); + digester.push(this); + + // Add all remote handlers + digester.addCallMethod("manifest/remote", "addRemote", 2); + digester.addCallParam("manifest/remote", 0, "name"); + digester.addCallParam("manifest/remote", 1, "fetch"); + + // Add all default handlers. This handles both repo format + // attributes (remote, revision) and non-standard attributes + // (branch, tag, commit-id). + digester.addCallMethod("manifest/default", "addDefault", 5); + digester.addCallParam("manifest/default", 0, "remote"); + digester.addCallParam("manifest/default", 1, "revision"); + digester.addCallParam("manifest/default", 2, "branch"); + digester.addCallParam("manifest/default", 3, "tag"); + digester.addCallParam("manifest/default", 4, "commit-id"); + + // Add all project handlers. This handles both repo format + // attributes (remote, revision) and non-standard attributes + // (branch, tag, commit-id). + digester.addCallMethod("manifest/project", "addProject", 6); + digester.addCallParam("manifest/project", 0, "remote"); + digester.addCallParam("manifest/project", 1, "name"); + digester.addCallParam("manifest/project", 2, "revision"); + digester.addCallParam("manifest/project", 3, "branch"); + digester.addCallParam("manifest/project", 4, "tag"); + digester.addCallParam("manifest/project", 5, "commit-id"); + + + InputStream input = new ByteArrayInputStream(contents); + try { + digester.parse(input); + } catch (IOException | SAXException e) { + log.warn("Unable to parse manifest", e); + } + HashMap<String, String> resolvedProjects = new HashMap<>(projects.size()); + for (Project p : projects) { + String uri = null; + String revision = null; + + if (p.getRemote() != null) { + uri = remotes.get(p.getRemote()); + } + + if (uri == null && defaultProject.getRemote() != null) { + uri = remotes.get(defaultProject.getRemote()); + } + + if (uri == null && p.getRemote() != null) { + uri = p.getRemote(); + } + + if (uri != null) { + uri += "/" + p.getName(); + } + + if (p.getRevision() != null) { + revision = p.getRevision(); + } else if (defaultProject.getRevision() != null) { + revision = defaultProject.getRevision(); + } + + if (uri != null && revision != null) { + resolvedProjects.put(uri, revision); + } else { + log.warn("Invalid project description in manifest"); + } + } + return resolvedProjects; + } + + public void addRemote(String name, String fetch) { + remotes.put(name, fetch); + } + + public void addDefault(String remote, String revision, String branch, + String tag, String commitId) { + defaultProject = + new Project(remote, null, getRevision(revision, branch, tag, commitId)); + } + + public void addProject(String remote, String name, String revision, + String branch, String tag, String commitId) { + if (name != null) { + projects.add(new Project(remote, name, + getRevision(revision, branch, tag, commitId))); + } else { + log.warn("Project name not specified in manifest"); + } + } + + private String getRevision(String revision, String branch, String tag, + String commitId) { + if (revision != null) { + return revision; + } else if (branch != null) { + return branch; + } else if (tag != null) { + return tag; + } else if (commitId != null) { + return commitId; + } + return null; + } + + public static class Project { + private String remote; + private String name; + private String revision; + + public Project(String remote, String name, String revision) { + this.remote = remote; + this.name = name; + this.revision = revision; + } + + public String getRemote() { + return remote; + } + + public String getName() { + return name; + } + + public String getRevision() { + return revision; + } + } + +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java new file mode 100644 index 0000000..4c7b822 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.repositoryuse; + +import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.internal.UniqueAnnotations; + +public class Module extends AbstractModule { + @Override + protected void configure() { + DynamicSet.bind(binder(), GitReferenceUpdatedListener.class) + .to(RefUpdateHandler.class); + requestStaticInjection(Config.class); + requestStaticInjection(Ref.Table.class); + requestStaticInjection(Usage.Table.class); + bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create()) + .to(SQLDriver.class); + } + + @Provides + @Singleton + SQLDriver provideSqlDriver() { + return new SQLDriver(); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Ref.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Ref.java new file mode 100644 index 0000000..58c74d2 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Ref.java
@@ -0,0 +1,203 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.repositoryuse; + +import com.google.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public class Ref { + private static final Logger log = LoggerFactory.getLogger(Ref.class); + private static Table table = new Table(); + + private String project; + private String ref; + private String commit; + private Date lastUpdated; + + public Ref(String project, String ref, String commit) { + init(project, ref, commit, new Date()); + } + + public Ref(String project, String ref, String commit, Date date) { + init(project, ref, commit, date); + } + + private void init(String project, String ref, String commit, Date date) { + this.project = project; + this.ref = ref; + this.commit = commit; + this.lastUpdated = date; + } + + public String getProject() { + return project; + } + + public String getRef() { + return ref; + } + + public String getCommit() { + return commit; + } + + public void setCommit(String commit) { + this.commit = commit; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void save() { + lastUpdated = new Date(); + table.insertOrUpdate(this); + log.info(String.format("Saving Ref: %s, %s, %s", project, ref, commit)); + } + + public void delete() { + log.info(String.format("Deleting Ref: %s, %s", project, ref)); + table.delete(this); + } + + public static List<Ref> fetchByProject(String project) { + return table.fetchByProject(project); + } + + public static Ref fetchByRef(String project, String ref) { + List<Ref> tmp = table.fetchByRef(project, ref); + if (tmp.size() == 1) { + return tmp.get(0); + } + return null; + } + + /* + * static interface Table { public void insertOrUpdate(Ref u); public + * List<Ref> fetchByProject(String project); public List<Ref> + * fetchByRef(String project, String ref); } + * + * public interface TableFactory { Table create(); } + */ + + static class Table { + private static final String TABLE_NAME = "RefStatus"; + private static final String PROJECT = "project"; + private static final String REF = "ref"; + private static final String COMMIT = "commit"; + private static final String DATE = "last_update"; + @Inject + private static SQLDriver sql; + + public Table() { + // Create the table if it doesn't exist + createTable(); + } + + private void createTable() { + StringBuilder query = new StringBuilder(); + query.append(String.format("CREATE TABLE IF NOT EXISTS %s(", TABLE_NAME)); + query.append(String.format("%s VARCHAR(1023),", PROJECT)); + query.append(String.format("%s VARCHAR(255),", REF)); + query.append(String.format("%s VARCHAR(40),", COMMIT)); + query.append(String.format("%s TIMESTAMP DEFAULT NOW(),", DATE)); + query.append(String.format("PRIMARY KEY (%s, %s))", PROJECT, REF)); + try { + sql.execute(query.toString()); + } catch (SQLException e) { + log.error("Unable to create Ref table", e); + } + } + + public void insertOrUpdate(Ref r) { + if (fetchByRef(r.getProject(), r.getRef()).isEmpty()) { + String query = "INSERT INTO " + TABLE_NAME + "(" + PROJECT + ", " + REF + + ", " + COMMIT + ", " + DATE + ") VALUES (?, ?, ?, " + + sql.getDateFormat() + ")"; + try { + sql.execute(query, r.getProject(), r.getRef(), r.getCommit(), + sql.getDateAsString(r.getLastUpdated())); + } catch (SQLException e) { + log.error("Unable to insert reference", e); + } + } else { + String query = "UPDATE " + TABLE_NAME + " SET " + COMMIT + "=?, " + DATE + + "=" + sql.getDateFormat() + " WHERE " + PROJECT + "=? AND " + REF + + "=?"; + try { + sql.execute(query, r.getCommit(), + sql.getDateAsString(r.getLastUpdated()), r.getProject(), + r.getRef()); + } catch (SQLException e) { + log.error("Unable to update reference", e); + } + } + } + + public void delete(Ref r) { + String query = "DELETE FROM " + TABLE_NAME + " WHERE " + PROJECT + + "=? AND " + REF + "=?"; + try { + sql.execute(query, r.getProject(), r.getRef()); + } catch (SQLException e) { + log.error("Unable to delete reference", e); + } + } + + + public List<Ref> fetchByProject(String project) { + String query = "SELECT " + PROJECT + ", " + REF + ", " + COMMIT + ", " + + DATE + " FROM " + TABLE_NAME + " WHERE " + PROJECT + "=?"; + try { + return loadRefs(sql.fetchRows(query, project)); + + } catch (SQLException e) { + log.error("Unable to execute query", e); + } + return Collections.emptyList(); + } + + public List<Ref> fetchByRef(String project, String ref) { + String query = "SELECT " + PROJECT + ", " + REF + ", " + COMMIT + ", " + + DATE + " FROM " + TABLE_NAME + " WHERE " + PROJECT + "=? AND " + REF + + "=?"; + try { + return loadRefs(sql.fetchRows(query, project, ref)); + } catch (SQLException e) { + log.error("Unable to execute query", e); + } + return Collections.emptyList(); + } + + private List<Ref> loadRefs(List<Map<String, String>> rows) { + List<Ref> result = new ArrayList<>(); + for (Map<String, String> row : rows) { + Ref tmp = new Ref(row.get(PROJECT), row.get(REF), row.get(COMMIT), + sql.getStringAsDate(row.get(DATE))); + result.add(tmp); + } + return result; + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java new file mode 100644 index 0000000..f3685b2 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java
@@ -0,0 +1,292 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.repositoryuse; + +import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.inject.Inject; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.submodule.SubmoduleWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.io.DisabledOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RefUpdateHandler implements GitReferenceUpdatedListener { + + private static final Logger log = + LoggerFactory.getLogger(RefUpdateHandler.class); + private final GitRepositoryManager repoManager; + private final String serverName; + + @Inject + RefUpdateHandler(GitRepositoryManager repoManager, + @CanonicalWebUrl String canonicalWebUrl) { + this.repoManager = repoManager; + if (canonicalWebUrl != null) { + try { + URL url = new URL(canonicalWebUrl); + canonicalWebUrl = url.getHost(); + } catch (MalformedURLException e) { + log.warn("Could not parse canonicalWebUrl", e); + } + } + this.serverName = canonicalWebUrl; + } + + @Override + public void onGitReferenceUpdated(Event event) { + if (event.isDelete() && event.getRefName().startsWith(Constants.R_HEADS) + || event.getRefName().startsWith(Constants.R_TAGS)) { + // Ref was deleted... clean up any references + Ref ref = Ref.fetchByRef(event.getProjectName(), event.getRefName()); + if (ref != null) { + ref.delete(); + } + if (event.getRefName().startsWith(Constants.R_HEADS)) { + // Also clean up uses from this ref + Usage.deleteByBranch(getCanonicalProject(event.getProjectName()), + event.getRefName()); + } + } else if (event.getRefName().startsWith(Constants.R_TAGS)) { + Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(), + event.getNewObjectId()); + updatedRef.save(); + } else if (event.getRefName().startsWith(Constants.R_HEADS)) { + Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(), + event.getNewObjectId()); + updatedRef.save(); + Project.NameKey nameKey = new Project.NameKey(event.getProjectName()); + try { + if (Config.refreshAllSubmodules() || event.isCreate() + || isSubmoduleUpdate(event, nameKey)) { + Map<String, String> submodules = getSubmodules(event, nameKey); + updateProjects(event.getProjectName(), event.getRefName(), + submodules); + } + if (Config.parseManifests()) { + parseManifests(event, nameKey); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + + private void parseManifests(Event event, Project.NameKey project) + throws RepositoryNotFoundException, IOException { + if (event.isDelete()) { + return; + } + try (Repository repo = repoManager.openRepository(project)) { + try (RevWalk walk = new RevWalk(repo); TreeWalk tw = new TreeWalk(repo)) { + RevCommit commit = + walk.parseCommit(repo.resolve(event.getNewObjectId())); + + tw.setRecursive(false); + tw.addTree(commit.getTree()); + ObjectReader or = tw.getObjectReader(); + while (tw.next()) { + String path = tw.getPathString(); + if (path.endsWith(".xml")) { + ManifestParser mp = new ManifestParser(); + ObjectLoader ol = or.open(tw.getObjectId(0)); + if (!ol.isLarge()) { + Map<String, String> tmp = mp.parseManifest(ol.getBytes()); + HashMap<String, String> projects = new HashMap<>(); + for (String key : tmp.keySet()) { + projects + .put( + normalizePath(String.format("%s:%s", + event.getProjectName(), path), key, true), + tmp.get(key)); + } + updateProjects( + String.format("%s:%s", event.getProjectName(), path), + event.getRefName(), projects); + } else { + log.warn(String.format("%s is too large, skipping manifest parse", + tw.getPathString())); + } + } + } + } + } + } + + /** + * Has a submodule been updated? + * + * @param event the Event + * @return True if a submodule update occurred, otherwise False. + */ + private boolean isSubmoduleUpdate(Event event, Project.NameKey project) + throws RepositoryNotFoundException, IOException { + if (event.isDelete()) { + return false; + } + try (Repository repo = repoManager.openRepository(project)) { + try (RevWalk walk = new RevWalk(repo); + DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) { + RevTree aTree = null; + if (!event.isCreate()) { + // If this is a new ref, we can't get the original commit. + // We can still use the DiffFormatter to give us what changed + // by passing null, however. + RevCommit aCommit = + walk.parseCommit(repo.resolve(event.getOldObjectId())); + aTree = aCommit.getTree(); + } + RevCommit bCommit = + walk.parseCommit(repo.resolve(event.getNewObjectId())); + RevTree bTree = bCommit.getTree(); + + df.setRepository(repo); + df.setDiffComparator(RawTextComparator.DEFAULT); + df.setDetectRenames(true); + List<DiffEntry> diffEntries = df.scan(aTree, bTree); + for (DiffEntry de : diffEntries) { + FileMode oldMode = de.getOldMode(); + FileMode newMode = de.getNewMode(); + if ((oldMode != null && oldMode == FileMode.GITLINK) + || (newMode != null && newMode == FileMode.GITLINK)) { + return true; + } + } + } + } + return false; + } + + private Map<String, String> getSubmodules(Event event, + Project.NameKey project) throws RepositoryNotFoundException, IOException { + HashMap<String, String> submodules = new HashMap<>(); + try (Repository repo = repoManager.openRepository(project)) { + try (RevWalk walk = new RevWalk(repo); + SubmoduleWalk sw = new SubmoduleWalk(repo)) { + RevCommit commit = + walk.parseCommit(repo.resolve(event.getNewObjectId())); + sw.setTree(commit.getTree()); + sw.setRootTree(commit.getTree()); + while (sw.next()) { + submodules.put( + normalizePath(event.getProjectName(), sw.getModulesUrl(), false), + sw.getObjectId().name()); + } + } catch (ConfigInvalidException e) { + log.warn("Invalid .gitmodules configuration while parsing " + + event.getProjectName()); + } + } + return submodules; + } + + private void updateProjects(String project, String branch, + Map<String, String> projects) { + String canonicalProject = getCanonicalProject(project); + List<Usage> uses = Usage.fetchByProject(canonicalProject, branch); + for (Usage use : uses) { + if (!projects.containsKey(use.getDestination())) { + // No longer exists; delete. + use.delete(); + } else { + // Update SHA1 here. + use.setRef(projects.get(use.getDestination())); + use.save(); + projects.remove(use.getDestination()); + } + } + // At this point, submodules only contains new elements. + // Create them. + for (String key : projects.keySet()) { + Usage use = new Usage(canonicalProject, branch, key, projects.get(key)); + use.save(); + } + } + + private String getCanonicalProject(String project) { + String canonicalProject = + String.format("https://%s/%s", serverName, project); + try { + URL url = new URL(canonicalProject); + canonicalProject = url.getHost() + url.getPath(); + } catch (MalformedURLException e) { + log.warn("Could not parse project as URL: " + canonicalProject); + } + return canonicalProject; + } + + private String normalizePath(String project, String destination, + boolean isManifest) { + String originalProject = + isManifest ? project.substring(0, project.lastIndexOf(":")) : project; + + // Handle relative and absolute paths on the same server + if (destination.startsWith("/")) { + if (serverName != null) { + destination = serverName + destination; + } else { + log.warn("Could not parse absolute path; canonicalWebUrl not set"); + } + } else if (destination.startsWith(".")) { + if (serverName != null) { + Path path = Paths.get(String.format("/%s/%s", project, destination)); + destination = serverName + path.normalize().toString(); + } else { + log.warn("Could not parse relative path; canonicalWebUrl not set"); + } + } else if (!destination.matches("^[^:]+://.*")) { + if (serverName != null) { + destination = serverName + "/" + originalProject + "/" + destination; + } else { + log.warn("Could not parse relative path; canonicalWebURl not set"); + } + } + + try { + // Replace the protocol with a known scheme, to avoid angering URL + destination = destination.replaceFirst("^[^:]+://", ""); + URL url = new URL("https://" + destination); + destination = url.getHost() + url.getPath(); + } catch (MalformedURLException e) { + log.warn("Could not parse destination as URL: " + destination); + } + return destination; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SQLDriver.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SQLDriver.java new file mode 100644 index 0000000..fdc6252 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SQLDriver.java
@@ -0,0 +1,158 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.repositoryuse; + +import com.google.gerrit.extensions.events.LifecycleListener; + +import com.googlesource.gerrit.plugins.repositoryuse.Config.Database; + +import org.apache.commons.dbcp.BasicDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SQLDriver implements LifecycleListener { + private static final Logger log = + LoggerFactory.getLogger(ManifestParser.class); + private static final int POOL_SIZE = 5; + private static SimpleDateFormat sdf = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + private BasicDataSource ds; + + public SQLDriver() { + ds = new BasicDataSource(); + try { + ds.setDriverClassName(getDriver()); + ds.setUrl(getDatabaseUrl()); + ds.setUsername(Config.getDatabaseUser()); + ds.setPassword(Config.getDatabasePassword()); + ds.setInitialSize(POOL_SIZE); + } catch (Exception e) { + log.error("Unable to create database connection", e); + } + } + + @Override + public void start() { + // no-op + } + + @Override + public void stop() { + if (ds != null) { + try { + ds.close(); + } catch (SQLException e) { + log.error("Unable to close connection", e); + } + } + } + + public List<Map<String, String>> fetchRows(String query, String... parameters) + throws SQLException { + ArrayList<Map<String, String>> result = new ArrayList<>(); + try (Connection c = ds.getConnection(); + PreparedStatement s = c.prepareStatement(query)) { + int i = 1; + for (String param : parameters) { + s.setString(i, param); + i++; + } + ResultSet r = s.executeQuery(); + ResultSetMetaData rsmd = r.getMetaData(); + while (r.next()) { + HashMap<String, String> row = new HashMap<>(rsmd.getColumnCount()); + for (i = 1; i <= rsmd.getColumnCount(); i++) { + row.put(rsmd.getColumnLabel(i).toLowerCase(), r.getString(i)); + } + result.add(row); + } + } + return result; + } + + public void execute(String query, String... parameters) throws SQLException { + try (Connection c = ds.getConnection(); + PreparedStatement s = c.prepareStatement(query)) { + int i = 1; + for (String param : parameters) { + s.setString(i, param); + i++; + } + if (!s.execute() && s.getUpdateCount() > 0) { + if (c.getAutoCommit() == false) { + c.commit(); + } + } + } + } + + public String getDateAsString(Date date) { + if (date != null) { + return sdf.format(date); + } + return sdf.format(new Date()); + } + + public Date getStringAsDate(String date) { + if (date != null) { + try { + return sdf.parse(date); + } catch (ParseException e) { + log.warn("Unable to parse date", e); + } + } + return new Date(); + } + + public String getDateFormat() { + if (Config.getDatabaseType() == Database.POSTGRESQL) { + return "TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS')"; + } + return "?"; + } + + private String getDriver() throws Exception { + if (Config.getDatabaseType() == Database.H2) { + return "org.h2.Driver"; + } else if (Config.getDatabaseType() == Database.POSTGRESQL) { + return "org.postgresql.Driver"; + } + throw new Exception("Unsupported database engine"); + } + + private String getDatabaseUrl() throws Exception { + if (Config.getDatabaseType() == Database.H2) { + return "jdbc:h2:" + Config.getDatabase(); + } else if (Config.getDatabaseType() == Database.POSTGRESQL) { + return "jdbc:postgresql://" + Config.getDatabaseHost() + "/" + + Config.getDatabase(); + } + throw new Exception("Unsupported database engine"); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Usage.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Usage.java new file mode 100644 index 0000000..5ca49a4 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Usage.java
@@ -0,0 +1,272 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.repositoryuse; + +import com.google.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public class Usage { + private static final Logger log = LoggerFactory.getLogger(Usage.class); + private static Table table = new Table(); + + + private String project; + private String branch; + private String destination; + private String ref; + private String info; + private Date lastUpdated; + + public Usage(String project, String branch, String destination, String ref) { + init(project, branch, destination, ref, null, new Date()); + } + + public Usage(String project, String branch, String destination, String ref, + String info) { + init(project, branch, destination, ref, info, new Date()); + } + + public Usage(String project, String branch, String destination, String ref, + String info, Date date) { + init(project, branch, destination, ref, info, date); + } + + private void init(String project, String branch, String destination, + String ref, String info, Date date) { + this.project = project; + this.branch = branch; + this.destination = destination; + this.ref = ref; + this.info = info; + this.lastUpdated = date; + } + + public String getProject() { + return project; + } + + public String getBranch() { + return branch; + } + + public String getDestination() { + return destination; + } + + public String getRef() { + return ref; + } + + public void setRef(String ref) { + this.ref = ref; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void save() { + lastUpdated = new Date(); + table.insertOrUpdate(this); + log.info(String.format("Saving Usage: %s, %s, %s, %s", project, branch, + destination, ref)); + } + + public void delete() { + table.delete(this); + log.info(String.format("Deleting Usage: %s, %s, %s", project, branch, + destination)); + } + + public static List<Usage> fetchByProject(String project) { + return table.fetchByProject(project); + } + + public static List<Usage> fetchByProject(String project, String branch) { + return table.fetchByProject(project, branch); + } + + public static List<Usage> fetchByDependency(String dependency) { + return table.fetchByDependency(dependency); + } + + public static void deleteByBranch(String project, String branch) { + table.deleteByBranch(project, branch); + log.info(String.format("Deleting all uses: %s, %s", project, branch)); + } + + static class Table { + private static final String TABLE_NAME = "RepoUsage"; + private static final String PROJECT = "project"; + private static final String BRANCH = "branch"; + private static final String DESTINATION = "destination"; + private static final String REF = "ref"; + private static final String INFO = "info"; + private static final String DATE = "last_update"; + @Inject + private static SQLDriver sql; + + public Table() { + // Create the table if it doesn't exist + createTable(); + } + + private void createTable() { + StringBuilder query = new StringBuilder(); + query.append(String.format("CREATE TABLE IF NOT EXISTS %s(", TABLE_NAME)); + query.append(String.format("%s VARCHAR(1023),", PROJECT)); + query.append(String.format("%s VARCHAR(255),", BRANCH)); + query.append(String.format("%s VARCHAR(1023),", DESTINATION)); + query.append(String.format("%s VARCHAR(255),", REF)); + query.append(String.format("%s VARCHAR(255),", INFO)); + query.append(String.format("%s TIMESTAMP DEFAULT NOW(),", DATE)); + query.append(String.format("PRIMARY KEY (%s, %s, %s))", PROJECT, BRANCH, + DESTINATION)); + try { + sql.execute(query.toString()); + } catch (SQLException e) { + log.error("Unable to create Usage table", e); + } + } + + public void insertOrUpdate(Usage u) { + if (fetchByProject(u.getProject(), u.getBranch(), u.getDestination()) + .isEmpty()) { + String query = "INSERT INTO " + TABLE_NAME + "(" + PROJECT + ", " + + BRANCH + ", " + DESTINATION + ", " + REF + ", " + INFO + ", " + + DATE + ") VALUES (?, ?, ?, ?, ?, " + sql.getDateFormat() + ")"; + try { + sql.execute(query, u.getProject(), u.getBranch(), u.getDestination(), + u.getRef(), u.getInfo(), sql.getDateAsString(u.getLastUpdated())); + } catch (SQLException e) { + log.error("Unable to insert usage", e); + } + } else { + String query = "UPDATE " + TABLE_NAME + " SET " + REF + "=?, " + INFO + + "=?, " + DATE + "=" + sql.getDateFormat() + " WHERE " + PROJECT + + "=? AND " + BRANCH + "=? AND " + DESTINATION + "=?"; + try { + sql.execute(query, u.getRef(), u.getInfo(), + sql.getDateAsString(u.getLastUpdated()), u.getProject(), + u.getBranch(), u.getDestination()); + } catch (SQLException e) { + log.error("Unable to update usage", e); + } + } + } + + public void delete(Usage u) { + String query = "DELETE FROM " + TABLE_NAME + " WHERE " + PROJECT + + "=? AND " + BRANCH + "=? AND " + DESTINATION + "=?"; + try { + sql.execute(query, u.getProject(), u.getBranch(), u.getDestination()); + } catch (SQLException e) { + log.error("Unable to delete usage", e); + } + } + + public void deleteByBranch(String project, String branch) { + String query = "DELETE FROM " + TABLE_NAME + " WHERE " + PROJECT + + "=? AND " + BRANCH + "=?"; + try { + sql.execute(query, project, branch); + } catch (SQLException e) { + log.error("Unable to delete usage", e); + } + } + + public List<Usage> fetchByProject(String project) { + String query = "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION + + ", " + REF + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME + + " WHERE " + PROJECT + "=?"; + try { + return loadUsage(sql.fetchRows(query, project)); + + } catch (SQLException e) { + log.error("Unable to execute query", e); + } + return Collections.emptyList(); + } + + public List<Usage> fetchByProject(String project, String branch) { + String query = "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION + + ", " + REF + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME + + " WHERE " + PROJECT + "=? AND " + BRANCH + "=?"; + try { + return loadUsage(sql.fetchRows(query, project, branch)); + + } catch (SQLException e) { + log.error("Unable to execute query", e); + } + return Collections.emptyList(); + } + + public List<Usage> fetchByProject(String project, String branch, + String destination) { + String query = + "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION + ", " + REF + + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME + " WHERE " + + PROJECT + "=? AND " + BRANCH + "=? AND " + DESTINATION + "=?"; + try { + return loadUsage(sql.fetchRows(query, project, branch, destination)); + + } catch (SQLException e) { + log.error("Unable to execute query", e); + } + return Collections.emptyList(); + } + + public List<Usage> fetchByDependency(String dependency) { + String query = "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION + + ", " + REF + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME + + " WHERE " + DESTINATION + "=?"; + try { + return loadUsage(sql.fetchRows(query, dependency)); + + } catch (SQLException e) { + log.error("Unable to execute query", e); + } + return Collections.emptyList(); + } + + private List<Usage> loadUsage(List<Map<String, String>> rows) { + List<Usage> result = new ArrayList<>(); + for (Map<String, String> row : rows) { + Usage tmp = new Usage(row.get(PROJECT), row.get(BRANCH), + row.get(DESTINATION), row.get(REF), row.get(INFO), + sql.getStringAsDate(row.get(DATE))); + result.add(tmp); + } + return result; + } + } +}