View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.shade.resource.rule;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.lang.annotation.Retention;
25  import java.lang.annotation.Target;
26  import java.nio.charset.StandardCharsets;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.jar.JarEntry;
31  import java.util.jar.JarInputStream;
32  import java.util.jar.JarOutputStream;
33  
34  import org.apache.maven.plugins.shade.relocation.Relocator;
35  import org.apache.maven.plugins.shade.resource.ReproducibleResourceTransformer;
36  import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
37  import org.codehaus.plexus.component.configurator.converters.ConfigurationConverter;
38  import org.codehaus.plexus.component.configurator.converters.lookup.ConverterLookup;
39  import org.codehaus.plexus.component.configurator.converters.lookup.DefaultConverterLookup;
40  import org.codehaus.plexus.component.configurator.expression.DefaultExpressionEvaluator;
41  import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
42  import org.codehaus.plexus.configuration.PlexusConfiguration;
43  import org.junit.rules.TestRule;
44  import org.junit.runner.Description;
45  import org.junit.runners.model.Statement;
46  
47  import static java.lang.annotation.ElementType.METHOD;
48  import static java.lang.annotation.RetentionPolicy.RUNTIME;
49  import static org.junit.Assert.assertNotNull;
50  import static org.junit.Assert.assertTrue;
51  import static org.junit.Assert.fail;
52  
53  public class TransformerTesterRule implements TestRule {
54      @Override
55      public Statement apply(Statement base, Description description) {
56          return new Statement() {
57              @Override
58              public void evaluate() throws Throwable {
59                  TransformerTest spec = description.getAnnotation(TransformerTest.class);
60                  if (spec == null) {
61                      base.evaluate();
62                      return;
63                  }
64  
65                  Map<String, String> jar;
66                  try {
67                      ReproducibleResourceTransformer transformer = createTransformer(spec);
68                      visit(spec, transformer);
69                      jar = captureOutput(transformer);
70                  } catch (Exception ex) {
71                      if (Exception.class.isAssignableFrom(spec.expectedException())) {
72                          assertTrue(
73                                  ex.getClass().getName(),
74                                  spec.expectedException().isAssignableFrom(ex.getClass()));
75                          return;
76                      } else {
77                          throw ex;
78                      }
79                  }
80                  asserts(spec, jar);
81              }
82          };
83      }
84  
85      private void asserts(TransformerTest spec, Map<String, String> jar) {
86          if (spec.strictMatch() && jar.size() != spec.expected().length) {
87              fail("Strict match test failed: " + jar);
88          }
89          for (final Resource expected : spec.expected()) {
90              final String content = jar.get(expected.path());
91              assertNotNull(expected.path(), content);
92              assertTrue(
93                      expected.path() + ", expected=" + expected.content() + ", actual=" + content,
94                      content.replace(System.lineSeparator(), "\n").matches(expected.content()));
95          }
96      }
97  
98      private Map<String, String> captureOutput(ReproducibleResourceTransformer transformer) throws IOException {
99          ByteArrayOutputStream out = new ByteArrayOutputStream();
100         try (JarOutputStream jar = new JarOutputStream(out)) {
101             transformer.modifyOutputStream(jar);
102         }
103 
104         Map<String, String> created = new HashMap<>();
105         try (JarInputStream jar = new JarInputStream(new ByteArrayInputStream(out.toByteArray()))) {
106             JarEntry entry;
107             while ((entry = jar.getNextJarEntry()) != null) {
108                 created.put(entry.getName(), read(jar));
109             }
110         }
111         return created;
112     }
113 
114     private void visit(TransformerTest spec, ReproducibleResourceTransformer transformer) throws IOException {
115         for (Resource resource : spec.visited()) {
116             if (transformer.canTransformResource(resource.path())) {
117                 transformer.processResource(
118                         resource.path(),
119                         new ByteArrayInputStream(resource.content().getBytes(StandardCharsets.UTF_8)),
120                         Collections.<Relocator>emptyList(),
121                         0);
122             }
123         }
124     }
125 
126     private String read(JarInputStream jar) throws IOException {
127         StringBuilder builder = new StringBuilder();
128         byte[] buffer = new byte[512];
129         int read;
130         while ((read = jar.read(buffer)) >= 0) {
131             builder.append(new String(buffer, 0, read));
132         }
133         return builder.toString();
134     }
135 
136     private ReproducibleResourceTransformer createTransformer(TransformerTest spec) {
137         ConverterLookup lookup = new DefaultConverterLookup();
138         try {
139             ConfigurationConverter converter = lookup.lookupConverterForType(spec.transformer());
140             PlexusConfiguration configuration = new DefaultPlexusConfiguration("configuration");
141             for (Property property : spec.configuration()) {
142                 configuration.addChild(property.name(), property.value());
143             }
144             return (ReproducibleResourceTransformer) converter.fromConfiguration(
145                     lookup,
146                     configuration,
147                     spec.transformer(),
148                     spec.transformer(),
149                     Thread.currentThread().getContextClassLoader(),
150                     new DefaultExpressionEvaluator());
151         } catch (ComponentConfigurationException e) {
152             throw new IllegalStateException(e);
153         }
154     }
155 
156     /**
157      * Enables to describe a test without having to implement the logic itself.
158      */
159     @Target(METHOD)
160     @Retention(RUNTIME)
161     public @interface TransformerTest {
162         /**
163          * @return the list of resource the transformer will process.
164          */
165         Resource[] visited();
166 
167         /**
168          * @return the expected output created by the transformer.
169          */
170         Resource[] expected();
171 
172         /**
173          * @return true if only expected resources must be found.
174          */
175         boolean strictMatch() default true;
176 
177         /**
178          * @return type of transformer to use.
179          */
180         Class<?> transformer();
181 
182         /**
183          * @return transformer configuration.
184          */
185         Property[] configuration();
186 
187         /**
188          * @return if set to an exception class it ensures it is thrown during the processing.
189          */
190         Class<?> expectedException() default Object.class;
191     }
192 
193     @Target(METHOD)
194     @Retention(RUNTIME)
195     public @interface Property {
196         String name();
197 
198         String value();
199     }
200 
201     @Target(METHOD)
202     @Retention(RUNTIME)
203     public @interface Resource {
204         String path();
205 
206         String content();
207     }
208 }