Issue #1577 - deploy-install and AWIPS II ant task updates

- includegen will now fail if a plugin is included in two or more features
- includegen will no longer prepend component-deploy.xml to the plugins in the list that it generates
- removed a plugin from the ncep feature that was also in the cots feature
- the includegen failure will cause deploy-install to fail
- peer review comments; excluding significant re-factoring

Change-Id: I2e317b1ef0764da3ddd9cd6ef10555ca4bf8bd3f

Former-commit-id: 7815b651bb [formerly 7659fb8044f3f4715c9ea7c875eda36b7c5dd136]
Former-commit-id: 5fea87f1dd
This commit is contained in:
Bryan Kowal 2013-02-05 11:05:55 -06:00
parent fa4dd004f9
commit 99994d3151
30 changed files with 1739 additions and 59 deletions

View file

@ -18,31 +18,9 @@
<not>
<equals arg1="${index}" arg2="1" />
</not>
<then>
<if>
<!-- TODO: we need our own ant plugin so this
becomes unnecessary [SPECIAL CASE]. -->
<equals arg1="@{plugin.type}" arg2="cots" />
<then>
<propertyregex property="plugin.name"
override="true"
input="@{line}"
regexp="(.+)/(.+)"
select="\1"
casesensitive="true" />
</then>
<else>
<propertyregex property="plugin.name"
override="true"
input="@{line}"
regexp="(.+)/component-deploy.xml"
select="\1"
casesensitive="true" />
</else>
</if>
<then>
<deployPlugins
plugin.name="${plugin.name}"
plugin.name="@{line}"
original.pattern="@{line}"
plugin.type="@{plugin.type}"
plugin.directories="@{plugin.directories}"

View file

@ -143,31 +143,9 @@
<not>
<equals arg1="${index}" arg2="1" />
</not>
<then>
<if>
<!-- TODO: we need our own ant plugin so this
becomes unnecessary [SPECIAL CASE]. -->
<equals arg1="@{plugin.type}" arg2="cots" />
<then>
<propertyregex property="plugin.name"
override="true"
input="@{line}"
regexp="(.+)/(.+)"
select="\1"
casesensitive="true" />
</then>
<else>
<propertyregex property="plugin.name"
override="true"
input="@{line}"
regexp="(.+)/component-deploy.xml"
select="\1"
casesensitive="true" />
</else>
</if>
<then>
<copyPlugin
plugin.name="${plugin.name}"
plugin.name="@{line}"
plugin.type="@{plugin.type}"
plugin.directories="@{plugin.directories}"
plugin.pattern="@{line}" />
@ -196,7 +174,7 @@
delimiter=";">
<sequential>
<if>
<available file="@{directory}/${plugin.name}" />
<available file="@{directory}/@{plugin.name}" />
<then>
<var name="plugin.directory" value="@{directory}" />
</then>
@ -207,13 +185,13 @@
<!-- copy the plugin to the plugins directory -->
<copy todir="${builder}/tmp/plugins">
<fileset dir="${plugin.directory}"
includes="${plugin.name}/**" />
includes="@{plugin.name}/**" />
</copy>
<if>
<equals arg1="@{plugin.type}" arg2="cots" />
<then>
<copyFOSS
plugin.directory323="${plugin.directory}/${plugin.name}"
plugin.directory323="${plugin.directory}/@{plugin.name}"
destination.directory323="${builder}/dependencies-stash/@{plugin.name}" />
</then>
</if>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>com.raytheon.uf.anttasks.includesgen</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ManifestBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.SchemaBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.PluginNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,8 @@
#Mon Feb 04 17:27:02 CST 2013
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6

View file

@ -0,0 +1,9 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Includesgen
Bundle-SymbolicName: com.raytheon.uf.anttasks.includesgen
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: RAYTHEON
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Require-Bundle: org.apache.ant;bundle-version="1.7.1",
com.raytheon.uf.featureexplorer;bundle-version="1.0.0"

View file

@ -0,0 +1,4 @@
source.. = src/
output.. = bin/
bin.includes = META-INF/,\
.

View file

@ -0,0 +1,354 @@
package com.raytheon.uf.anttasks.includesgen;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import com.raytheon.uf.featureexplorer.FeatureException;
import com.raytheon.uf.featureexplorer.FeatureExplorer;
/**
* Custom Ant task for generating includes files based on feature.xml
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 bclement Initial creation
* 28Jan2009 1930 MW Fegan Added provider and plugin filtering.
* Feb 4, 2013 #1577 bkowal Remove component-deploy.xml suffixes for
* core and plugins; remove jar wildcard for cots
* </pre>
*
* @author bclement
* @version 1.0
*/
public class GenerateIncludesFromFeature extends Task {
protected String baseDirectories;
protected File baseDirectory;
protected File featureFile;
protected File cotsOut;
protected File plugsOut;
protected File coreOut;
protected File allOut;
/* attributes for filtering */
private static final String PATTERN = "^.*(%s).*$";
protected String provider = "raytheon";
private Pattern providerPattern = Pattern.compile(String.format(PATTERN,
provider));
protected String plugin = "plugin";
private Pattern pluginPattern = Pattern.compile(String.format(PATTERN,
plugin));
/**
* @return the baseDirectories
*/
public String getBaseDirectories() {
return baseDirectories;
}
/**
* @param baseDirectories
* the baseDirectories to set
*/
public void setBaseDirectories(String baseDirectories) {
this.baseDirectories = baseDirectories;
}
/**
* Sets the component provider filter. This is a pipe (|) separated string
* of names that identify a component as a non-COTS component. The default
* provider filter is <em>raytheon</em>.
*/
public void setProviderFilter(String filter) {
this.provider = filter;
this.providerPattern = Pattern.compile(String.format("^.*(%s).*$",
filter));
}
/**
* Sets the plug-in identifier filter. This is a pipe (|) separated string
* of names that identify a component as an EDEX plug-in. The default
* plug-in filter is <em>plugin</em>.
*/
public void setPluginFilter(String filter) {
this.plugin = filter;
this.pluginPattern = Pattern.compile(String
.format("^.*(%s).*$", filter));
}
public void setAllOut(File f) {
this.allOut = f;
}
public void setFeatureFile(File f) {
this.featureFile = f;
}
public void setCotsOut(File f) {
this.cotsOut = f;
}
public void setPlugsOut(File f) {
this.plugsOut = f;
}
public void setCoreOut(File f) {
this.coreOut = f;
}
public void setBaseDirectory(File aDir) {
this.baseDirectory = aDir;
}
/**
* Main class called from ant. Tests to see if at least 1 includes file is
* to be generated. Only generates includes that have been specified from
* ant script. Throws BuildException if any error occurs, this halts the ant
* build and displays the error.
*
*/
public void execute() throws BuildException {
log("provider filter=" + this.providerPattern.toString());
log("plugin filter=" + this.pluginPattern.toString());
ArrayList<File> components = getComponents();
if (cotsOut == null && plugsOut == null && coreOut == null
&& allOut == null)
throw new BuildException(
"Must supply destination for at least one includes file");
if (featureFile == null)
throw new BuildException("Must supply a feature.xml file");
if (cotsOut != null)
generateCots(components);
if (plugsOut != null)
generatePlugs(components);
if (coreOut != null)
generateCore(components);
if (allOut != null)
generateAll(components);
}
/**
* Generates an includes file for all components in the feature. The
* directories in comps will be added to the includes file with a recursive
* reg ex. This means that the director and all of its sub directories and
* files will be included.
*
* @param comps
*/
protected void generateAll(ArrayList<File> comps) {
log("Generating ALL list in " + this.allOut.getPath());
PrintWriter out = null;
try {
out = new PrintWriter(new FileWriter(allOut));
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss z");
format.setTimeZone(TimeZone.getTimeZone("GMT"));
out.println("## Feature includes file generated on "
+ format.format(new Date()));
for (File f : comps) {
out.println(f.getName() + "/**");
}
log(String.format("Identified %d ALL list entries", comps.size()));
} catch (IOException e) {
throw new BuildException(e);
} finally {
if (out != null) {
out.close();
}
}
}
/**
* Populates a list of project directories with one directory for each
* component stated in feature. This list is built based on manifest ids and
* version compared to what is stated in the feature. These can differ
* drastically from the name of project.
*
* @return a populated list of directories based off feature
* @throws BuildException
* if there are any problems accessing feature or finding
* required components from feature.
*/
protected ArrayList<File> getComponents() throws BuildException {
ArrayList<File> rval = null;
FeatureExplorer fe = null;
// list of directories overrides single directory
if (baseDirectories != null) {
String[] fileNames = baseDirectories.split(";");
ArrayList<File> files = new ArrayList<File>(fileNames.length);
for (String fName : fileNames) {
File file = new File(fName);
files.add(file);
}
fe = new FeatureExplorer(new WorkspaceFeatureSearch(files),
new WorkspacePluginSearch(files));
} else if (baseDirectory != null) {
fe = new FeatureExplorer(baseDirectory, new WorkspaceFeatureSearch(
baseDirectory), new WorkspacePluginSearch(baseDirectory));
} else {
throw new BuildException(
"Did not have a baseDirectory or baseDirectories");
}
try {
rval = fe.getPlugins(featureFile);
} catch (FeatureException e) {
throw new BuildException(e);
}
if (rval.isEmpty()) {
throw new BuildException("Unable to access file " + featureFile);
}
return rval;
}
/**
* Generates an includes file for all components (comps) having a project
* directory name that does not match the {@link #setProviderFilter(String)
* provider filter}. The project's manifest will be searched for a list of
* jars to specify in the includes file. If the list is not present in the
* manifest, the default is to include all *.jar files in the includes file.
*
* @param comps
* The full list of feature specified components
* @throws BuildException
* if any IOException occurs
*/
protected void generateCots(ArrayList<File> comps) throws BuildException {
log("Generating COTS list in " + this.cotsOut.getPath());
PrintWriter out = null;
try {
out = new PrintWriter(new FileWriter(cotsOut));
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss z");
format.setTimeZone(TimeZone.getTimeZone("GMT"));
out.println("## Cots includes file generated on "
+ format.format(new Date()));
int count = 0;
for (File f : comps) {
Matcher m = providerPattern.matcher(f.getName());
if (!m.matches()) {
out.println(f.getName());
count++;
}
}
log(String.format("Identified %d COTS list entries", count));
} catch (IOException e) {
throw new BuildException(e);
} finally {
if (out != null) {
out.close();
}
}
}
/**
* Generates an includes file for all components (comps) having a project
* directory name that matches both the {@link #setProviderFilter(String)
* provider filter} and the {@link #setPluginFilter(String) plug-in filter}.
*
* @param comps
* The full list of feature specified components
* @throws BuildException
* if any IOException occurs
*/
protected void generatePlugs(ArrayList<File> comps) throws BuildException {
log("Generating PLUGS list in " + this.plugsOut.getPath());
PrintWriter out = null;
try {
out = new PrintWriter(new FileWriter(plugsOut));
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss z");
format.setTimeZone(TimeZone.getTimeZone("GMT"));
out.println("## Plug-in includes file generated on "
+ format.format(new Date()));
int count = 0;
for (File f : comps) {
Matcher prm = providerPattern.matcher(f.getName());
Matcher plm = pluginPattern.matcher(f.getName());
if (prm.matches() && plm.matches()) {
out.println(f.getName());
count++;
}
}
log(String.format("Identified %d PLUGS list entries", count));
} catch (IOException e) {
throw new BuildException(e);
} finally {
if (out != null) {
out.close();
}
}
}
/**
* Generates an includes file for all components (comps) having a project
* directory name that matches the {@link #setProviderFilter(String)
* provider filter} and does not match the {@link #setPluginFilter(String)
* plug-in filter}.
*
* @param comps
* The full list of feature specified components
* @throws BuildException
* if any IOException occurs
*/
protected void generateCore(ArrayList<File> comps) throws BuildException {
log("Generating CORE list in " + this.coreOut.getPath());
PrintWriter out = null;
try {
out = new PrintWriter(new FileWriter(coreOut));
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss z");
format.setTimeZone(TimeZone.getTimeZone("GMT"));
out.println("## Core includes file generated on "
+ format.format(new Date()));
int count = 0;
for (File f : comps) {
Matcher prm = providerPattern.matcher(f.getName());
Matcher plm = pluginPattern.matcher(f.getName());
if (prm.matches() && !plm.matches()) {
out.println(f.getName());
count++;
}
}
log(String.format("Identified %d CORE list entries", count));
} catch (IOException e) {
throw new BuildException(e);
} finally {
if (out != null) {
out.close();
}
}
}
}

View file

@ -0,0 +1,57 @@
package com.raytheon.uf.anttasks.includesgen;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.raytheon.uf.featureexplorer.FeatureIndex;
import com.raytheon.uf.featureexplorer.search.IFeatureSearch;
/**
* Feature search that is used in the FeatureExplorer class as a custom way to
* search for features.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 bclement Initial creation
* Jan 5, 2013 #1577 bkowal changed basedirs declaration to List from ArrayList
* </pre>
*
* @author bclement
* @version 1.0
*/
public class WorkspaceFeatureSearch implements IFeatureSearch {
private List<File> baseDirs;
public WorkspaceFeatureSearch(File base) {
this.baseDirs = new ArrayList<File>(1);
this.baseDirs.add(base);
}
public WorkspaceFeatureSearch(ArrayList<File> baseDirs) {
this.baseDirs = baseDirs;
}
@Override
/**
* Finds a list of feature locations in which all have the ID anId and whose
* version is greater than or equal to aVersion. The location with the
* latest version will be at index 0 in the list.
*
*/
public List<File> findFeature(String anId, String aVersion) {
List<File> rval = new ArrayList<File>();
try {
FeatureIndex index = new FeatureIndex(baseDirs);
rval = index.query(anId, aVersion);
} catch (IOException e) {
e.printStackTrace();
}
return rval;
}
}

View file

@ -0,0 +1,57 @@
package com.raytheon.uf.anttasks.includesgen;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.raytheon.uf.featureexplorer.PluginManifestIndex;
import com.raytheon.uf.featureexplorer.search.IPluginSearch;
/**
* Plugin search that is used in the FeatureExplorer class as a custom way to
* search for plugins.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 bclement Initial creation
* Jan 5, 2013 #1577 bkowal changed basedirs declaration to List from ArrayList
* </pre>
*
* @author bclement
* @version 1.0
*/
public class WorkspacePluginSearch implements IPluginSearch {
private List<File> baseDirs;
public WorkspacePluginSearch(File base) {
this.baseDirs = new ArrayList<File>(1);
this.baseDirs.add(base);
}
public WorkspacePluginSearch(ArrayList<File> baseDirs) {
this.baseDirs = baseDirs;
}
@Override
/**
* Finds a list of plugin locations in which all have the ID anId and whose
* version is greater than or equal to aVersion. The location with the
* latest version will be at index 0 in the list.
*
*/
public List<File> findPlugin(String anId, String aVersion) {
List<File> rval = new ArrayList<File>();
try {
PluginManifestIndex index = new PluginManifestIndex(baseDirs);
rval = index.query(anId, aVersion);
} catch (IOException e) {
e.printStackTrace();
}
return rval;
}
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>com.raytheon.uf.featureexplorer</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ManifestBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.SchemaBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.PluginNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,8 @@
#Mon Feb 04 17:21:26 CST 2013
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6

View file

@ -0,0 +1,10 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Featureexplorer
Bundle-SymbolicName: com.raytheon.uf.featureexplorer
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: RAYTHEON
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Export-Package: com.raytheon.uf.featureexplorer,
com.raytheon.uf.featureexplorer.jaxb,
com.raytheon.uf.featureexplorer.search

View file

@ -0,0 +1,4 @@
source.. = src/
output.. = bin/
bin.includes = META-INF/,\
.

View file

@ -0,0 +1,169 @@
package com.raytheon.uf.featureexplorer;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class is to be used for indexing sets of manifests that are found in a
* plug-in project for Eclipse.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 bclement Initial creation
* Oct 6, 2008 dglazesk Added a function that adds a directory to
* the index
* </pre>
*
* @author bclement
* @version 1.0
*/
abstract public class AbstractResourceIndex {
/**
* File object for the base directory.
*/
protected File baseDir;
/**
* Structure that houses the information.
*
* index[id][ver] == path to directory with id and version ver
*/
protected Map<String, Map<String, File>> index;
/**
* Create the index based on a base directory. The base directory must
* exist.
*
* @param aBaseDirectory
* The base directory where all the projects are subdirectories
*/
public AbstractResourceIndex(File aBaseDirectory) {
index = new HashMap<String, Map<String, File>>();
indexDirectory(aBaseDirectory);
}
public AbstractResourceIndex(Collection<File> aBaseDirectories) {
index = new HashMap<String, Map<String, File>>();
for (File file : aBaseDirectories) {
indexDirectory(file);
}
}
/**
* Adds a particular directory to the index. It doesn't add anything if it
* is an invalid directory (doesn't exist | it's not a directory | it isn't
* readable).
*
* @param aBaseDirectory
* Base directory to be added to the index
*/
public void indexDirectory(File aBaseDirectory) {
if (!aBaseDirectory.exists() || !aBaseDirectory.isDirectory()
|| !aBaseDirectory.canRead())
return;
baseDir = aBaseDirectory;
File[] directories = getSubDirectories();
for (File dir : directories) {
catalog(dir);
}
}
/**
* This function does all of the work. It creates entries in the map for the
* IDs and the version numbers for the project into the map.
*
* @param projectDirectory
* Directory for the project to look into
*/
abstract protected void catalog(File projectDirectory);
/**
* Finds the sub-directories of the base directory.
*
* @return The array of sub-directories for the base directory
*/
public File[] getSubDirectories() {
return baseDir.listFiles(new FileFilter() {
/**
* This function accepts if the file is a directory.
*
* @param f
* The file being checked for acceptance
* @return True if the file is a directory
*/
@Override
public boolean accept(File f) {
return f.isDirectory();
}
});
}
/**
* Returns the base directory for this index.
*
* @return File object representing the base for the index
*/
public File getBaseDirectory() {
return this.baseDir;
}
/**
* Returns the map of items that maps the version number to the location on
* the filesystem for the desired project.
*
* @param anId
* @return
*/
public Map<String, File> getVersions(String anId) {
return index.get(anId);
}
/**
* Queries the index in search of a particular ID with an associated version
* that is greater than aVersion. This will return an empty list if the
* query results in an empty set.
*
* @param anId
* The ID of the plugin being searched for
* @param aVersion
* The minimum version of the plugin
* @return List of file objects representing the locations of the results of
* the query in descending order based on version
*/
public List<File> query(String anId, String aVersion) {
List<File> rval = new ArrayList<File>();
// get the versions associated with this id
Map<String, File> verFileMap = this.getVersions(anId);
if (verFileMap != null) {
// if we have something, look for all of the versions that are
// greater than aVersion
Version base = new Version(aVersion);
List<Version> versions = new ArrayList<Version>();
for (String version : verFileMap.keySet()) {
Version cur = new Version(version);
if (base.compareTo(cur) <= 0) {
versions.add(cur);
}
}
// sort the list of versions and create the return list
Collections.sort(versions, new ReverseVersionComparator());
for (Version version : versions) {
rval.add(verFileMap.get(version.toString()));
}
}
return rval;
}
}

View file

@ -0,0 +1,52 @@
package com.raytheon.uf.featureexplorer;
/**
* Base exception for Feature Explorer
*
* <pre>
*
* SOFTWARE HISTORY
*
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* 9/7/2008 SP#15 bclement Initial Creation.
*
* </pre>
*
* @author bclement
*
*/
public class FeatureException extends Exception {
private static final long serialVersionUID = 3812733413272988454L;
/**
* Default Constructor
*
*/
public FeatureException() {
}
/**
* @param message
*/
public FeatureException(String message) {
super(message);
}
/**
* @param message
* @param cause
*/
public FeatureException(Throwable cause) {
super(cause);
}
/**
* @param cause
*/
public FeatureException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,287 @@
package com.raytheon.uf.featureexplorer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import com.raytheon.uf.featureexplorer.jaxb.Feature;
import com.raytheon.uf.featureexplorer.jaxb.Includes;
import com.raytheon.uf.featureexplorer.jaxb.Plugin;
import com.raytheon.uf.featureexplorer.search.IFeatureSearch;
import com.raytheon.uf.featureexplorer.search.IPluginSearch;
/**
* This represents an interface for a user to search through a feature for
* plugins. This class utilizes other interfaces to do the actual searches, but
* handles the bundling of the final list as well as the recursion into included
* features.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 dglazesk Initial creation
* Oct 7, 2008 SP#15 bclement Added FeatureException code
* Oct 7, 2008 SP#15 bclement Changed ArrayList references to List
* Oct 10, 2008 SP#15 bclement Added static functions for reading manifests
* Feb 4, 2013 #1577 bkowal Verify that a plugin has not been included in more than one feature.
* </pre>
*
* @author dglazesk
* @version 1.0
*/
public class FeatureExplorer {
/**
* This holds the feature searching object for the class.
*/
protected IFeatureSearch featureSearch;
/**
* This holds the plugin searching object for the class.
*/
protected IPluginSearch pluginSearch;
/**
* This holds the feature file object should the user choose to set it.
*/
protected File feature = null;
private Map<String, File> pluginLookupMap = new HashMap<String, File>();
/**
* This constructor allows a user to setup what the feature for this
* instance is. This allows the user to use getPlugins(), though the other
* methods are still available.
*
* @param aFeature
* File object for the feature for this instance
* @param aFeatureSearch
* The object that can search for features
* @param aPluginSearh
* The object that can search for plugins
*/
public FeatureExplorer(File aFeature, IFeatureSearch aFeatureSearch,
IPluginSearch aPluginSearh) {
featureSearch = aFeatureSearch;
pluginSearch = aPluginSearh;
feature = aFeature;
}
/**
* This constructor sets up the classes that will be used for searching for
* plugins and features. It is expected that a user will use
* getPlugins(File) or getPlugins(String) later when getting the plugins.
*
* @param aFeatureSearch
* The object that can search for features
* @param aPluginSearh
* The object that can search for plugins
*/
public FeatureExplorer(IFeatureSearch aFeatureSearch,
IPluginSearch aPluginSearh) {
featureSearch = aFeatureSearch;
pluginSearch = aPluginSearh;
}
/**
* This is a convenience method for when the feature file object is set in a
* constructor.
*
* @return The list of files in the feature for this instance
* @throws FeatureException
*/
public ArrayList<File> getPlugins() throws FeatureException {
return getPlugins(feature);
}
/**
* This is just a convenience method for getting plugins from a feature.
* This is equivalent to doing getPlugins(new File(aPath)).
*
* @param aPath
* Path to the feature.xml file to be scanned
* @return The list of file objects for all of the plugins in the feature
* @throws FeatureException
*/
public ArrayList<File> getPlugins(String aPath) throws FeatureException {
File feat = new File(aPath);
return getPlugins(feat);
}
/**
* This function attempts to find all of the plugins associated with the
* feature. This includes recursing into any included features and grabbing
* their plugins.
*
* @param aFeature
* The file object for the feature to be scanned
* @return The list of file objects for the located plugins
* @throws FeatureException
*/
public ArrayList<File> getPlugins(File aFeature) throws FeatureException {
ArrayList<File> rval = new ArrayList<File>();
HashMap<String, File> plugins = getFeaturePlugins(aFeature);
rval = new ArrayList<File>(plugins.values());
return rval;
}
/**
* This finds all of the plugins listed in a feature and maps their ids to
* their file system locations as file objects. This is the function that
* does the brunt of the work in locating the plugins.
*
* @param aFile
* The feature file that is being scanned
* @return A hash map that links the plugin id to its file system location
* @throws FeatureException
* If there are any problems with JAXB, a feature cannot be
* found, or a plugin cannot be found
*/
protected HashMap<String, File> getFeaturePlugins(File aFile)
throws FeatureException {
HashMap<String, File> rval = new HashMap<String, File>();
if (aFile == null || !aFile.exists() || !aFile.canRead())
return rval;
Feature feat = null;
try {
JAXBContext jc = JAXBContext.newInstance(Feature.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
feat = (Feature) unmarshaller.unmarshal(aFile);
} catch (Exception e) {
throw new FeatureException("Unable to unmarshal file " + aFile, e);
}
for (Includes include : feat.getIncludes()) {
// go through all of the included features and try to find them
List<File> features = featureSearch.findFeature(include.getId(),
include.getVersion());
try {
// get all of the plugin id to file objects and add them
rval.putAll(getFeaturePlugins(features.get(0)));
} catch (IndexOutOfBoundsException e) {
if (!include.getOptional()) {
// this means we received an empty list, no feature found
throw new FeatureException("Could not find feature "
+ include.getId() + " with version greater than "
+ include.getVersion());
}
}
}
for (Plugin plugin : feat.getPlugins()) {
// go through all of the mentioned plugins
List<File> plugs = pluginSearch.findPlugin(plugin.getId(),
plugin.getVersion());
try {
if (this.pluginLookupMap.containsKey(plugin.getId())
&& this.pluginLookupMap.get(plugin.getId()) != aFile) {
StringBuilder stringBuilder = new StringBuilder("Plugin ");
stringBuilder.append(plugin.getId());
stringBuilder.append(" is in Feature ");
stringBuilder.append(this.generateFeatureFileName(
aFile.getParent(), aFile.getName()));
stringBuilder.append(" and Feature ");
stringBuilder
.append(this.generateFeatureFileName(
this.pluginLookupMap.get(plugin.getId())
.getParent(), this.pluginLookupMap
.get(plugin.getId()).getName()));
stringBuilder.append("!");
throw new FeatureException(stringBuilder.toString());
}
// add the plugin id and its file object to the map
rval.put(plugin.getId(), plugs.get(0));
this.pluginLookupMap.put(plugin.getId(), aFile);
} catch (IndexOutOfBoundsException e) {
// this means we received an empty list, no plugin found
throw new FeatureException("Could not find plugin "
+ plugin.getId() + " with version greater than "
+ plugin.getVersion());
}
}
return rval;
}
private String generateFeatureFileName(String parentPath, String fileName) {
String[] pathElements = parentPath.split(File.separator);
return pathElements[pathElements.length - 1] + File.separator
+ fileName;
}
/**
* Searches a project's manifest for a specific attribute. The returned list
* will contain all values for the attribute or empty if not found.
*
* @param projectRoot
* @param attrib
* @return a list of a values for the attribute or an empty list if not
* found
* @throws IOException
*/
public static List<String> readManifest(File projectRoot, String attrib)
throws IOException {
File maniFile = new File(projectRoot, "/META-INF/MANIFEST.MF");
Manifest m = null;
List<String> rval = new ArrayList<String>();
try {
m = new Manifest();
// we only care if the manifest actually exists
InputStream is = new FileInputStream(maniFile);
m.read(is);
is.close();
} catch (IOException e) {
throw new IOException(
"IO Error while reading manifest for project: "
+ projectRoot.getName());
}
// if we get this far, m shouldn't be null
if (m != null) {
Attributes attribs = m.getMainAttributes();
String deploys = attribs.getValue(attrib);
// manifests that do not have a deploy entry will return a wildcard
if (deploys != null) {
for (String s : deploys.split(",")) {
rval.add(s.trim());
}
}
}
return rval;
}
/**
* Reads the manifest for the project and returns all values for the
* "Edex-Deploy" attribute. If the attribute could not be found, default to
* returning a wildcard for all jars.
*
* @param projectRoot
* @return a list of jar names or a wildcard for all jars if attribute not
* found
* @throws IOException
*/
public static List<String> getJars(File projectRoot) throws IOException {
List<String> rval = readManifest(projectRoot, "Edex-Deploy");
if (rval.isEmpty()) {
rval.add("*.jar");
}
return rval;
}
}

View file

@ -0,0 +1,81 @@
package com.raytheon.uf.featureexplorer;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import com.raytheon.uf.featureexplorer.jaxb.Feature;
/**
* This class represents an index of feature.xml files in a certain directory.
* This also has querying abilities thanks to the abstract class.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 6, 2008 bclement Initial creation
* </pre>
*
* @author bclement
* @version 1.0
*/
public class FeatureIndex extends AbstractResourceIndex {
/**
* Needed constructor that sets the base directory for the index.
*
* @param aBaseDirectory
* File object for the base directory to start the index in
* @throws IOException
*/
public FeatureIndex(Collection<File> aBaseDirectories) throws IOException {
super(aBaseDirectories);
}
/**
* This function does all of the work. It looks for a feature in the project
* directory and creates entries in the map for the IDs and the version
* numbers for the project into the map.
*
* @param projectDirectory
* Directory for the project to look into
* @see AbstractResourceIndex#catalog(File)
*/
protected void catalog(File projectDirectory) {
File featureFile = new File(projectDirectory, "feature.xml");
if (featureFile.exists()) {
try {
// using JAXB to do the unmarshalling of feature.xml
JAXBContext jc = JAXBContext.newInstance(Feature.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
Feature feat = (Feature) unmarshaller.unmarshal(featureFile);
String id = feat.getId();
String version = feat.getVersion();
// ignore features that do not have a bundle name and version
if (id != null && version != null) {
if (index.containsValue(id)) {
index.get(id).put(version, projectDirectory);
} else {
Map<String, File> versionMap = new HashMap<String, File>();
versionMap.put(version, featureFile);
index.put(id, versionMap);
}
}
} catch (JAXBException e) {
e.printStackTrace();
}
}
}
}

View file

@ -0,0 +1,118 @@
package com.raytheon.uf.featureexplorer;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
* This class is to be used for indexing sets of manifests that are found in a
* plug-in project for Eclipse.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 bclement Initial creation
* Oct 6, 2008 dglazesk Added support for indexing the jar files.
* This just helps with Eclipse plugin indexing.
* </pre>
*
* @author bclement
* @version 1.0
*/
public class PluginManifestIndex extends AbstractResourceIndex {
/**
* Create the index based on a base directory. The base directory must
* exist.
*
* @param aBaseDirectory
* The base directory where all the plugins are subdirectories
* @throws IOException
* Thrown if the directory does not exist or is not a directory.
*/
public PluginManifestIndex(Collection<File> aBaseDirectories)
throws IOException {
super(aBaseDirectories);
}
/**
* This function does all of the work. It looks for a manifest in the
* "META-INF" folder in the project directory and creates entries in the map
* for the IDs and the version numbers for the project into the map.
*
* @param projectDirectory
* Directory for the project to look into
*/
protected void catalog(File projectDirectory) {
File maniFile = new File(projectDirectory, "META-INF/MANIFEST.MF");
Manifest m = null;
try {
if (projectDirectory.isFile()) {
// regular file means it may be a jar, so try it
JarFile proj = new JarFile(projectDirectory);
m = proj.getManifest();
} else if (maniFile.exists()) {
m = new Manifest();
// we only care if the manifest actually exists
InputStream is = new FileInputStream(maniFile);
m.read(is);
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
// if m is null, we either didn't have the manifest or there was an
// exception, skip
if (m != null) {
Attributes attribs = m.getMainAttributes();
String id = attribs.getValue("Bundle-SymbolicName");
String version = attribs.getValue("Bundle-Version");
// ignore manifests that do not have a bundle name and version
if (id != null && version != null) {
if (index.containsValue(id)) {
index.get(id).put(version, projectDirectory);
} else {
Map<String, File> versionMap = new HashMap<String, File>();
versionMap.put(version, projectDirectory);
index.put(id, versionMap);
}
}
}
}
/**
* Finds the sub-directories and any jar files in the base directory. The
* plugin manifest index needs to support indexing the plug-in jars as well.
*
* @return The array of sub-directories or jars in the base directory
*/
@Override
public File[] getSubDirectories() {
return baseDir.listFiles(new FileFilter() {
/**
* This function accepts if the file is a directory or is a jar
* file.
*
* @param f
* The file being checked for acceptance
* @return True if the file is a directory or a jar
*/
@Override
public boolean accept(File f) {
return f.isDirectory() || f.getName().endsWith(".jar");
}
});
}
}

View file

@ -0,0 +1,38 @@
package com.raytheon.uf.featureexplorer;
import java.util.Comparator;
/**
* This class is available for the sort function of Collections. The idea is
* that it compares the Versions backwards so that the sort creates a descending
* order Collection.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 dglazesk Initial creation
* </pre>
*
* @author dglazesk
* @version 1.0
*/
public class ReverseVersionComparator implements Comparator<Version> {
/**
* This comparator is purposefully backwards so that the highest version
* number can be sorted to the front of an array.
*
* @param left
* The left hand side of the version comparison
* @param right
* The right hand side of the version comparison
* @return -1 if left is bigger than right, 1 if right is bigger, and 0 if
* equal
*/
@Override
public int compare(Version left, Version right) {
return right.compareTo(left);
}
}

View file

@ -0,0 +1,111 @@
package com.raytheon.uf.featureexplorer;
/**
* This class represents a version number that is separated by periods(.). The
* length doesn't matter because the array sizes based on a string split. This
* is also capable of comparing two version numbers of any length and
* determining which is greater. TODO Add Description
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 dglazesk Initial creation
* </pre>
*
* @author dglazesk
* @version 1.0
*/
public class Version implements Comparable<Version> {
/**
* This is meant to hold the string version as passed in to the constructor.
*/
private String versionString;
/**
* This holds the split up version of the string version with all split
* parts interpreted as ints.
*/
private int[] intVersion;
/**
* Create a version and set the version string as version. This also sets up
* the intVersion for comparison purposes.
*
* @param version
* The string for the version number
*/
public Version(String version) {
versionString = version;
String[] nums = version.split("[.]");
intVersion = new int[nums.length];
for (int i = 0; i < nums.length; ++i) {
try {
intVersion[i] = Integer.parseInt(nums[i]);
} catch (NumberFormatException e) {
intVersion[i] = 0;
}
}
}
/**
* Get the integer array version of the version number. Good for comparison.
*
* @return The integer array for the version as parsed in constructor
*/
public int[] getIntVersion() {
return intVersion;
}
/**
* Returns the passed in version string.
*
* @return The string version as passed in to the constructor
*/
public String toString() {
return versionString;
}
/**
* Comparing function for the comparable interface. Returns -1 if the other
* version number is greater. If the version numbers differ in the number of
* integers, zeros will be added to the number that is shorter.
*
* @param otherVersion
* The version object we are comparing against.
* @return -1 if otherVersion is bigger, 1 if this is bigger, and 0 if equal
*/
@Override
public int compareTo(Version otherVersion) {
int[] otherIntVersion = otherVersion.getIntVersion();
// set the limit for the loop as bigger length
int limit = intVersion.length;
if (otherIntVersion.length > intVersion.length)
limit = otherIntVersion.length;
int me = 0;
int you = 0;
for (int i = 0; i < limit; ++i) {
// me looking at you looking at me
me = you = me = 0;
// me still have ints
if (intVersion.length > i)
me = intVersion[i];
// you still have ints
if (otherIntVersion.length > i)
you = otherIntVersion[i];
// first difference in number tells us the greater one
if (me > you)
return 1;
else if (me < you)
return -1;
}
return 0;
}
}

View file

@ -0,0 +1,95 @@
package com.raytheon.uf.featureexplorer.jaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class Feature {
@XmlAttribute
private String id;
@XmlAttribute
private String label;
@XmlAttribute
private String version;
@XmlAttribute(name = "provider-name")
private String providerName;
@XmlElements( { @XmlElement(name = "includes", type = Includes.class) })
private List<Includes> includes;
@XmlElements( { @XmlElement(name = "plugin", type = Plugin.class) })
private List<Plugin> plugins;
public Feature() {
this.includes = new ArrayList<Includes>();
this.plugins = new ArrayList<Plugin>();
}
public Feature(String anId, String aLabel, String aVersion,
String aProviderName, List<Includes> includesList,
List<Plugin> pluginsList) {
this.includes = new ArrayList<Includes>(includesList);
this.plugins = new ArrayList<Plugin>(pluginsList);
this.id = anId;
this.label = aLabel;
this.version = aVersion;
this.providerName = aProviderName;
}
public List<Includes> getIncludes() {
return this.includes;
}
public List<Plugin> getPlugins() {
return this.plugins;
}
public String getId() {
return this.id;
}
public String getLabel() {
return this.label;
}
public String getVersion() {
return this.version;
}
public String getProviderName() {
return this.providerName;
}
public void setId(String anId) {
this.id = anId;
}
public void setLabel(String aLabel) {
this.label = aLabel;
}
public void setVersion(String aVersion) {
this.version = aVersion;
}
public void setProviderName(String aProviderName) {
this.providerName = aProviderName;
}
public void setPlugins(List<Plugin> pluginsList) {
this.plugins = new ArrayList<Plugin>(pluginsList);
}
}

View file

@ -0,0 +1,51 @@
package com.raytheon.uf.featureexplorer.jaxb;
import javax.xml.bind.annotation.XmlAccessOrder;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorOrder;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
@XmlAccessorType(XmlAccessType.NONE)
@XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
public class Includes {
@XmlAttribute
private String id;
@XmlAttribute
private String version;
@XmlAttribute
private boolean optional = false;
public Includes() {
}
public Includes(String anId, String aVersion, boolean isOptional) {
this.id = anId;
this.version = aVersion;
this.optional = isOptional;
}
public void setId(String anId) {
this.id = anId;
}
public void setVersion(String aVersion) {
this.version = aVersion;
}
public String getId() {
return this.id;
}
public String getVersion() {
return this.version;
}
public boolean getOptional() {
return this.optional;
}
}

View file

@ -0,0 +1,79 @@
package com.raytheon.uf.featureexplorer.jaxb;
import javax.xml.bind.annotation.XmlAccessOrder;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorOrder;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
@XmlAccessorType(XmlAccessType.NONE)
@XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
public class Plugin {
@XmlAttribute
private String id;
@XmlAttribute(name = "download-size")
private double downloadSize;
@XmlAttribute
private String version;
@XmlAttribute(name = "install-size")
private double installSize;
@XmlAttribute
private boolean unpack;
public Plugin() {
}
public Plugin(String anId, double aDownloadSize, double anInstallSize,
String aVersion, boolean doUnpack) {
this.id = anId;
this.downloadSize = aDownloadSize;
this.version = aVersion;
this.installSize = anInstallSize;
this.unpack = doUnpack;
}
public String getId() {
return this.id;
}
public double getDownloadSize() {
return this.downloadSize;
}
public String getVersion() {
return this.version;
}
public double getInstallSize() {
return this.installSize;
}
public boolean getUnpack() {
return this.unpack;
}
public void setId(String anId) {
this.id = anId;
}
public void setDownloadSize(double aDownloadSize) {
this.downloadSize = aDownloadSize;
}
public void setVersion(String aVersion) {
this.version = aVersion;
}
public void setInstallSize(double anInstallSize) {
this.installSize = anInstallSize;
}
public void setUnpack(boolean doUnpack) {
this.unpack = doUnpack;
}
}

View file

@ -0,0 +1,35 @@
package com.raytheon.uf.featureexplorer.search;
import java.io.File;
import java.util.List;
/**
* This interface is used for creating objects that are capable of searching for
* a feature.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 dglazesk Initial creation
* Oct 7, 2008 SP#15 bclement changed return value to List
* </pre>
*
* @author dglazesk
* @version 1.0
*/
public interface IFeatureSearch {
/**
* This will search for a feature that has anId as its ID and has a version
* greater than or equal to aVersion. The final list will be ordered by
* version number with the highest version number at element 0.
*
* @param anId
* ID of the feature being searched for
* @param aVersion
* Version minimum for the feature
* @return List of file objects for the feature.xml that match the search
* criteria
*/
public List<File> findFeature(String anId, String aVersion);
}

View file

@ -0,0 +1,35 @@
package com.raytheon.uf.featureexplorer.search;
import java.io.File;
import java.util.List;
/**
* This interface is used for creating objects that are capable of searching for
* a plugins.
*
* <pre>
* SOFTWARE HISTORY
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Oct 3, 2008 dglazesk Initial creation
* Oct 7, 2008 SP#15 bclement changed return value to List
* </pre>
*
* @author dglazesk
* @version 1.0
*/
public interface IPluginSearch {
/**
* This will search for a plugin with anId as its ID and has a version
* number greater than or equal to aVersion. The final list will be ordered
* by version number with the highest version at element 0.
*
* @param anId
* The ID of the plugin being searched for
* @param aVersion
* The minimum version of the plugin
* @return List of file locations for the plugins matching the search
* criteria
*/
public List<File> findPlugin(String anId, String aVersion);
}

View file

@ -24,14 +24,6 @@
version="0.0.0"
unpack="false"/>
<plugin
id="gov.nasa.gsfc.fits"
download-size="0"
install-size="0"
version="0.0.0"
unpack="false"/>
<plugin
id="gov.noaa.nws.ncep.common.dataplugin.airmet"
download-size="0"