Skip to content

Commit

Permalink
Fixed #96: reserved template manifest properties are preserved (#97)
Browse files Browse the repository at this point in the history
* Added jar manifest reader
* Fixed #96: Reserved manifest properties are preserved
  • Loading branch information
duncanp-sonar authored Aug 8, 2018
1 parent 642c400 commit 10a1c69
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 54 deletions.
83 changes: 83 additions & 0 deletions PluginGenerator/JarManifestReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* SonarQube Roslyn SDK
* Copyright (C) 2015-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using System.Collections.Generic;

namespace SonarQube.Plugins
{
/// <summary>
/// Reads a valid v1.0 Jar Manifest.
/// See http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#JAR%20Manifest
/// </summary>
public class JarManifestReader
{
private readonly Dictionary<string, string> kvps;
private const string SEPARATOR = ": ";

public JarManifestReader(string manifestText)
{
if (manifestText == null)
{
throw new ArgumentNullException(nameof(manifestText));
}

kvps = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

// A manifest file line can have at most 72 characters. Long values are split across
// multiple lines, with the continuation line starting with a single space.
// The simplest way to rejoin the lines is just to replace all (EOL + space) with EOL
var joinedText = manifestText.Replace("\r\n ", string.Empty);
var lines = joinedText.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);

// Every line should now be a key-value pair
foreach (var line in lines)
{
var index = line.IndexOf(SEPARATOR);

if (index < 0)
{
throw new InvalidOperationException(
string.Format(System.Globalization.CultureInfo.CurrentCulture, UIResources.Reader_Error_InvalidManifest, line));
}

var key = line.Substring(0, index);
var value = line.Substring(index + SEPARATOR.Length);
kvps[key] = value;
}
}

public string FindValue(string key)
{
kvps.TryGetValue(key, out string value);
return value;
}

public string GetValue(string key)
{
if (!kvps.TryGetValue(key, out string value))
{
throw new InvalidOperationException(
string.Format(System.Globalization.CultureInfo.CurrentCulture, UIResources.Reader_Error_MissingManifestSetting, key));
}
return value;
}
}
}
1 change: 1 addition & 0 deletions PluginGenerator/SonarQube.Plugins.PluginGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<Compile Include="DataModel\Rule.cs" />
<Compile Include="DataModel\Rules.cs" />
<Compile Include="JarManifestBuilder.cs" />
<Compile Include="JarManifestReader.cs" />
<Compile Include="PluginKeyUtilities.cs" />
<Compile Include="RepositoryKeyUtilities.cs" />
<Compile Include="PluginDefinition.cs" />
Expand Down
20 changes: 19 additions & 1 deletion PluginGenerator/UIResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions PluginGenerator/UIResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,10 @@
<data name="JMan_Error_NameTooLong" xml:space="preserve">
<value>The attribute name is too long. Maximum 70 chars. Name: {0}</value>
</data>
<data name="Reader_Error_InvalidManifest" xml:space="preserve">
<value>Manifest file is not valid - line does not contain a key-value separator: '{0}'</value>
</data>
<data name="Reader_Error_MissingManifestSetting" xml:space="preserve">
<value>The expected setting was not found in the manifest file: {0}</value>
</data>
</root>
4 changes: 2 additions & 2 deletions RoslynPluginGenerator/ArchiveUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ public void UpdateArchive()
logger.LogDebug(UIResources.ZIP_JarUpdated, outputArchiveFilePath);
}

#endregion Public methods

private void DoUpdate()
{
using (ZipArchive newArchive = new ZipArchive(new FileStream(outputArchiveFilePath, FileMode.Open), ZipArchiveMode.Update))
Expand Down Expand Up @@ -121,7 +123,5 @@ private ZipArchiveEntry GetOrCreateEntry(ZipArchive archive, string fullEntryNam
return archive.CreateEntry(fullEntryName);

}

#endregion Public methods
}
}
96 changes: 67 additions & 29 deletions RoslynPluginGenerator/RoslynPluginJarBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using SonarQube.Plugins.Common;

namespace SonarQube.Plugins.Roslyn
Expand All @@ -35,16 +36,11 @@ public class RoslynPluginJarBuilder
/// </summary>
private const string TemplateJarResourceName = "SonarQube.Plugins.Roslyn.Resources.sonar-roslyn-sdk-template-plugin-1.1.jar";

/// <summary>
/// The name of the plugin class in the embedded jar file
/// </summary>
private const string PluginClassName = "org.sonar.plugins.roslynsdk.RoslynSdkGeneratedPlugin";

// Locations in the jar where various file should be embedded
private const string RelativeManifestResourcePath = "META-INF\\MANIFEST.MF";

private const string RelativeConfigurationResourcePath = "org\\sonar\\plugins\\roslynsdk\\configuration.xml";
private const string RelativeRulesXmlResourcePath = "org\\sonar\\plugins\\roslynsdk\\rules.xml";
// Locations in the jar archive where various file should be embedded.
// Using forward-slash since that is the separator used by Java for archive entry names.
private const string RelativeManifestResourcePath = "META-INF/MANIFEST.MF";
private const string RelativeConfigurationResourcePath = "org/sonar/plugins/roslynsdk/configuration.xml";
private const string RelativeRulesXmlResourcePath = "org/sonar/plugins/roslynsdk/rules.xml";

private readonly ILogger logger;

Expand All @@ -65,8 +61,6 @@ public RoslynPluginJarBuilder(ILogger logger)
pluginProperties = new Dictionary<string, string>();
jarManifestBuilder = new JarManifestBuilder();
fileToRelativePathMap = new Dictionary<string, string>();

SetFixedManifestProperties();
}

public RoslynPluginJarBuilder SetJarFilePath(string filePath)
Expand Down Expand Up @@ -178,7 +172,6 @@ public RoslynPluginJarBuilder SetRulesFilePath(string filePath)
return this;
}


public RoslynPluginJarBuilder SetRepositoryKey(string key)
{
RepositoryKeyUtilities.ThrowIfInvalid(key);
Expand Down Expand Up @@ -230,12 +223,13 @@ public void BuildJar(string workingDirectory)

ValidateConfiguration();

string templateJarFilePath = ExtractTemplateJarFile(workingDirectory);

// Create the config and manifest files
string configFilePath = BuildConfigFile(workingDirectory);
string manifestFilePath = jarManifestBuilder.WriteManifest(workingDirectory);
string manifestFilePath = BuildManifest(templateJarFilePath, workingDirectory);

// Update the jar
string templateJarFilePath = ExtractTemplateJarFile(workingDirectory);
ArchiveUpdater updater = new ArchiveUpdater(this.logger);

updater.SetInputArchive(templateJarFilePath)
Expand Down Expand Up @@ -315,20 +309,6 @@ private string FindPluginKey()
return pluginKey;
}

/// <summary>
/// Sets the invariant, required manifest properties
/// </summary>
private void SetFixedManifestProperties()
{
// This property must appear first in the manifest.
// See http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#JAR%20Manifest
jarManifestBuilder.SetProperty("Sonar-Version", "4.5.2");
jarManifestBuilder.SetProperty("Plugin-Dependencies", "META-INF/lib/sslr-squid-bridge-2.6.jar");
jarManifestBuilder.SetProperty("Plugin-SourcesUrl", "https://github.com/SonarSource-VisualStudio/sonarqube-roslyn-sdk-template-plugin");

jarManifestBuilder.SetProperty("Plugin-Class", PluginClassName);
}

private static string ExtractTemplateJarFile(string workingDirectory)
{
string templateJarFilePath = Path.Combine(workingDirectory, "template.jar");
Expand All @@ -345,6 +325,64 @@ private static string ExtractTemplateJarFile(string workingDirectory)
return templateJarFilePath;
}

private string BuildManifest(string templateJarFilePath, string workingDirectory)
{
var templateManifest = GetContentsOfFileFromArchive(templateJarFilePath, RelativeManifestResourcePath);

CopyReservedPropertiesFromExistingManifest(templateManifest);

var manifestFilePath = jarManifestBuilder.WriteManifest(workingDirectory);
return manifestFilePath;
}

private string GetContentsOfFileFromArchive(string pathToArchive, string fullEntryName)
{
string text = null;
using (var archive = new ZipArchive(new FileStream(pathToArchive, FileMode.Open)))
{
var entry = archive.GetEntry(fullEntryName);
if (entry == null)
{
throw new InvalidOperationException(
string.Format(System.Globalization.CultureInfo.CurrentCulture,
UIResources.Builder_Error_EntryNotFoundInTemplatePlugin, fullEntryName));
}

var buffer = new byte[entry.Length];
using (var entryStream = entry.Open())
{
entryStream.Read(buffer, 0, buffer.Length);
text = System.Text.Encoding.UTF8.GetString(buffer);
}
}

return text;
}

/// <summary>
/// Some of the properties in the template manifest should
/// be preserved e.g. the supported SonarQube version
/// </summary>
private void CopyReservedPropertiesFromExistingManifest(string templateManifest)
{
var reader = new JarManifestReader(templateManifest);
CopyValueFromExistingManifest(reader, "Sonar-Version");
CopyValueFromExistingManifest(reader, "Plugin-Dependencies");
CopyValueFromExistingManifest(reader, "Plugin-Class");
CopyValueFromExistingManifest(reader, "SonarLint-Supported"); // applies to other IDEs i.e. not VS
}

private void CopyValueFromExistingManifest(JarManifestReader reader, string property)
{
if (jarManifestBuilder.TryGetValue(property, out string value))
{
throw new InvalidOperationException(
string.Format(System.Globalization.CultureInfo.CurrentCulture,
UIResources.Builder_Error_ManifestPropertyShouldBeCopiedFromTemplate, property));
}
jarManifestBuilder.SetProperty(property, reader.GetValue(property));
}

#endregion Private methods configuration
}
}
18 changes: 18 additions & 0 deletions RoslynPluginGenerator/UIResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions RoslynPluginGenerator/UIResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ Rules definitions: a template rules xml file for the analyzer was saved to {0}.
<data name="APG_UsingSuppliedRulesFile" xml:space="preserve">
<value>Using the supplied rules xml file: {0}</value>
</data>
<data name="Builder_Error_EntryNotFoundInTemplatePlugin" xml:space="preserve">
<value>Entry not found in template plugin: {0}</value>
</data>
<data name="Builder_Error_ManifestPropertyShouldBeCopiedFromTemplate" xml:space="preserve">
<value>Manifest property should be copied from the template jar, not set directly: {0}</value>
</data>
<data name="Builder_Error_OutputJarPathMustBeSpecified" xml:space="preserve">
<value>The file path for the jar file to be created must be specified</value>
</data>
Expand Down
Loading

0 comments on commit 10a1c69

Please sign in to comment.