1717
1818import  java .awt .Desktop ;
1919import  java .io .File ;
20+ import  java .io .IOException ;
21+ import  java .io .InputStream ;
22+ import  java .net .MalformedURLException ;
2023import  java .net .URL ;
2124import  java .nio .charset .StandardCharsets ;
25+ import  java .nio .file .Files ;
26+ import  java .nio .file .Path ;
27+ import  java .nio .file .Paths ;
28+ import  java .nio .file .StandardCopyOption ;
2229import  java .sql .Connection ;
23- import  java .util .ArrayList ;
24- import  java .util .Arrays ;
25- import  java .util .HashMap ;
26- import  java .util .List ;
27- import  java .util .Map ;
30+ import  java .util .*;
31+ import  java .util .jar .JarEntry ;
32+ import  java .util .jar .JarFile ;
2833import  java .util .logging .Logger ;
2934import  java .util .stream .Collectors ;
3035
36+ import  org .springframework .core .io .Resource ;
37+ import  org .springframework .core .io .support .PathMatchingResourcePatternResolver ;
38+ import  org .springframework .core .io .support .ResourcePatternResolver ;
3139import  org .utplsql .sqldev .dal .RealtimeReporterDao ;
3240import  org .utplsql .sqldev .dal .UtplsqlDao ;
3341import  org .utplsql .sqldev .exception .GenericDatabaseAccessException ;
4250
4351public  class  CodeCoverageReporter  {
4452    private  static  final  Logger  logger  = Logger .getLogger (CodeCoverageReporter .class .getName ());
53+     private  static  final  String  ASSETS_PATH  = "coverage/assets/" ;
4554
4655    private  String  connectionName ;
4756    private  Connection  conn ;
48-     private  List <String > pathList ;
49-     private  List <String > includeObjectList ;
57+     private  final   List <String > pathList ;
58+     private  final   List <String > includeObjectList ;
5059    private  CodeCoverageReporterDialog  frame ;
5160    private  String  schemas ;
5261    private  String  includeObjects ;
5362    private  String  excludeObjects ;
63+     private  Path  assetDir ;
5464
5565    public  CodeCoverageReporter (final  List <String > pathList , final  List <String > includeObjectList ,
5666            final  String  connectionName ) {
5767        this .pathList  = pathList ;
5868        this .includeObjectList  = includeObjectList ;
5969        setDefaultSchema ();
6070        setConnection (connectionName );
71+         setAssetDir ();
6172    }
6273
6374    // constructor for testing purposes only 
@@ -67,6 +78,7 @@ public CodeCoverageReporter(final List<String> pathList, final List<String> incl
6778        this .includeObjectList  = includeObjectList ;
6879        this .conn  = conn ;
6980        setDefaultSchema ();
81+         setAssetDir ();
7082    }
7183
7284    private  void  setConnection (final  String  connectionName ) {
@@ -105,6 +117,59 @@ private void setDefaultSchema() {
105117        }
106118    }
107119
120+     private  void  setAssetDir () {
121+         try  {
122+             assetDir  = Files .createTempDirectory ("utplsql_assets_" );
123+         } catch  (IOException  e ) {
124+             throw  new  GenericRuntimeException ("Cannot create temporary directory for code coverage report assets." , e );
125+         }
126+         populateCoverageAssets ();
127+     }
128+ 
129+     // public for testing purposes only 
130+     public  URL  getHtmlReportAssetPath () {
131+         try  {
132+             return  Paths .get (assetDir .toString ()).toUri ().toURL ();
133+         } catch  (MalformedURLException  e ) {
134+             throw  new  GenericRuntimeException ("Cannot convert code coverage asset path to URL." , e );
135+         }
136+     }
137+ 
138+     private  void  copyStreamToFile (InputStream  inputStream , Path  file ) throws  IOException  {
139+         file .toFile ().mkdirs ();
140+         Files .copy (inputStream , file , StandardCopyOption .REPLACE_EXISTING );
141+     }
142+ 
143+     private  void  populateCoverageAssets () {
144+         logger .fine (() -> "Copying code coverage report assets to "  + assetDir .toString () + "..." );
145+         try  {
146+             final  File  file  = new  File (getClass ().getProtectionDomain ().getCodeSource ().getLocation ().getPath ());
147+             if  (file .isFile ()) {
148+                 // class loaded from a JAR file 
149+                 final  JarFile  jar  = new  JarFile (file );
150+                 final  List <JarEntry > entries  = jar .stream ().filter (entry  -> !entry .isDirectory () && entry .getName ().startsWith (ASSETS_PATH )).collect (Collectors .toList ());
151+                 for  (JarEntry  entry  : entries ) {
152+                     Path  f  = Paths .get (assetDir .toString () + File .separator  + entry .getName ().substring (ASSETS_PATH .length ()));
153+                     copyStreamToFile (jar .getInputStream (entry ), f );
154+                 }
155+             } else  {
156+                 // class loaded from file system (IDE or during test/build) 
157+                 ResourcePatternResolver  resolver  = new  PathMatchingResourcePatternResolver ();
158+                 Resource [] resources  = resolver .getResources (ResourcePatternResolver .CLASSPATH_ALL_URL_PREFIX  + "/"  + ASSETS_PATH  + "**" );
159+                 for  (Resource  resource  : resources ) {
160+                     if  (Objects .requireNonNull (resource .getFilename ()).contains ("." )) {
161+                         // process files but not directories, assume that directories do not contain a period 
162+                         String  path  = resource .getURL ().getPath ();
163+                         Path  f  = Paths .get (assetDir .toString () + File .separator  + path .substring (path .lastIndexOf (ASSETS_PATH ) + ASSETS_PATH .length ()));
164+                         copyStreamToFile (resource .getInputStream (), f );
165+                     }
166+                 }
167+             }
168+         } catch  (IOException  e ) {
169+             throw  new  GenericRuntimeException ("Error while copying coverage report assets to temporary directory." , e );
170+         }
171+     }
172+ 
108173    private  ArrayList <String > toStringList (final  String  s ) {
109174        final  ArrayList <String > list  = new  ArrayList <>();
110175        if  (s  != null  && !s .isEmpty ()) {
@@ -142,7 +207,7 @@ private void run() {
142207
143208    private  void  runCodeCoverageWithRealtimeReporter () {
144209        final  UtplsqlRunner  runner  = new  UtplsqlRunner (pathList , toStringList (schemas ), toStringList (includeObjects ),
145-                 toStringList (excludeObjects ), connectionName );
210+                 toStringList (excludeObjects ), getHtmlReportAssetPath (),  connectionName );
146211        runner .runTestAsync ();
147212    }
148213
@@ -152,7 +217,7 @@ private void runCodeCoverageStandalone() {
152217            coverageConn  = conn  != null  ? conn  : DatabaseTools .cloneConnection (connectionName );
153218            final  UtplsqlDao  dao  = new  UtplsqlDao (coverageConn );
154219            final  String  html  = dao .htmlCodeCoverage (pathList , toStringList (schemas ),
155-                     toStringList (includeObjects ), toStringList (excludeObjects ));
220+                     toStringList (includeObjects ), toStringList (excludeObjects ),  getHtmlReportAssetPath () );
156221            openInBrowser (html );
157222        } finally  {
158223            try  {
@@ -174,7 +239,7 @@ public static void openInBrowser(String html) {
174239            final  URL  url  = file .toURI ().toURL ();
175240            logger .fine (() -> "Opening "  + url .toExternalForm () + " in browser..." );
176241            final  Desktop  desktop  = Desktop .isDesktopSupported () ? Desktop .getDesktop () : null ;
177-             if  (desktop  != null  && desktop .isSupported (Desktop .Action .BROWSE ) &&  url  !=  null ) {
242+             if  (desktop  != null  && desktop .isSupported (Desktop .Action .BROWSE )) {
178243                desktop .browse (url .toURI ());
179244                logger .fine (() -> url .toExternalForm () + " opened in browser." );
180245            } else  {
@@ -229,9 +294,7 @@ public void setExcludeObjects(final String excludeObjects) {
229294    }
230295
231296    public  Thread  runAsync () {
232-         final  Thread  thread  = new  Thread (() -> {
233-             run ();
234-         });
297+         final  Thread  thread  = new  Thread (this ::run );
235298        thread .setName ("code coverage reporter" );
236299        thread .start ();
237300        return  thread ;
0 commit comments