Lines 58-316
import java.util.regex.Matcher;
Link Here
|
58 |
import java.util.regex.Matcher; |
58 |
import java.util.regex.Matcher; |
59 |
import java.util.regex.Pattern; |
59 |
import java.util.regex.Pattern; |
60 |
import junit.framework.Assert; |
60 |
import junit.framework.Assert; |
|
|
61 |
import junit.framework.Test; |
61 |
import junit.framework.TestResult; |
62 |
import junit.framework.TestResult; |
62 |
import org.openide.util.Lookup; |
63 |
import org.openide.util.Lookup; |
63 |
|
64 |
|
64 |
/** |
65 |
/** |
|
|
66 |
* Wraps a test class with proper NetBeans Runtime Container environment. |
67 |
* This allows to execute tests in a very similar environment to the |
68 |
* actual invocation in the NetBeans IDE. To use write your test as |
69 |
* you are used to and add suite static method: |
70 |
* <pre> |
71 |
* public class YourTest extends NbTestCase { |
72 |
* public YourTest(String s) { super(s); } |
73 |
* |
74 |
* public static Test suite() { |
75 |
* return NbModuleSuite.create(YourTest.class); |
76 |
* } |
77 |
* |
78 |
* public void testXYZ() { ... } |
79 |
* public void testABC() { ... } |
80 |
* } |
81 |
* </pre> |
65 |
* |
82 |
* |
66 |
* @author Jaroslav Tulach <jaroslav.tulach@netbeans.org> |
83 |
* @author Jaroslav Tulach <jaroslav.tulach@netbeans.org> |
67 |
*/ |
84 |
*/ |
68 |
public class NbModuleSuite extends NbTestSuite { |
85 |
public final class NbModuleSuite extends Object { |
69 |
private final Class<?> clazz; |
|
|
70 |
private final String clusterRegExp; |
71 |
|
86 |
|
72 |
public NbModuleSuite(Class<?> aClass) { |
87 |
/** Wraps the provided class into a test that set ups properly the |
73 |
this(aClass, ".*"); |
88 |
* testing environment. The set of enabled modules is going to be |
74 |
} |
89 |
* determined from the actual classpath of a module, which is common |
75 |
|
90 |
* when in all NetBeans tests. All other modules are kept disabled. |
76 |
public NbModuleSuite(Class<?> aClass, String clusterRegExp) { |
91 |
* <p> |
77 |
super(); |
92 |
* |
78 |
this.clazz = aClass; |
93 |
* Warning: because the NetBeans Runtime Environment |
79 |
this.clusterRegExp = clusterRegExp; |
94 |
* plays various tricks with classloaders, the class provided as argument |
|
|
95 |
* is just a <quote>template</quote>, the one that will really be executed |
96 |
* is loaded by another classloader. As such it is not recommended to |
97 |
* do any static initializations in the class, as that would be run twice, |
98 |
* in different modes and could cause quite a big mishmash. |
99 |
* |
100 |
* @param clazz the class with bunch of testXYZ methods |
101 |
* @return test that starts the NetBeans Runtime Container and then runs the |
102 |
*/ |
103 |
public static Test create(Class<? extends Test> clazz) { |
104 |
return new S(clazz); |
80 |
} |
105 |
} |
81 |
|
106 |
|
82 |
@Override |
107 |
/** Factory method to create wrapper test that knows how to setup proper |
83 |
public void run(TestResult result) { |
108 |
* NetBeans Runtime Container environment. In addition to other factory |
84 |
try { |
109 |
* methods, it allows one limit the clusters that shall be made available. |
85 |
runInRuntimeContainer(result); |
110 |
* For example <code>ide.*|java.*</code> will start the container just |
86 |
} catch (Exception ex) { |
111 |
* with platform, ide and java clusters. |
87 |
result.addError(this, ex); |
112 |
* |
88 |
} |
113 |
* |
89 |
} |
114 |
* @param clazz the class with bunch of testXYZ methods |
90 |
|
115 |
* @param clustersRegExp regexp to apply to name of cluster to find out if it is supposed to be included |
91 |
private void runInRuntimeContainer(TestResult result) throws Exception { |
116 |
* in the runtime container setup or not |
92 |
File platform = findPlatform(); |
117 |
* @return runtime container ready test |
93 |
File[] boot = new File(platform, "lib").listFiles(); |
118 |
*/ |
94 |
List<URL> bootCP = new ArrayList<URL>(); |
119 |
public static Test create(Class<? extends Test> clazz, String clustersRegExp) { |
95 |
for (int i = 0; i < boot.length; i++) { |
120 |
return new S(clazz); |
96 |
URL u = boot[i].toURL(); |
|
|
97 |
if (u.toExternalForm().endsWith(".jar")) { |
98 |
bootCP.add(u); |
99 |
} |
100 |
} |
101 |
// loader that does not see our current classloader |
102 |
ClassLoader parent = ClassLoader.getSystemClassLoader().getParent(); |
103 |
Assert.assertNotNull("Parent", parent); |
104 |
URLClassLoader loader = new URLClassLoader(bootCP.toArray(new URL[0]), parent); |
105 |
Class<?> main = loader.loadClass("org.netbeans.Main"); // NOI18N |
106 |
Assert.assertEquals("Loaded by our classloader", loader, main.getClassLoader()); |
107 |
Method m = main.getDeclaredMethod("main", String[].class); // NOI18N |
108 |
|
109 |
System.setProperty("java.util.logging.config", "-"); |
110 |
System.setProperty("netbeans.home", platform.getPath()); |
111 |
|
112 |
File ud = new File(new File(Manager.getWorkDirPath()), "userdir"); |
113 |
ud.mkdirs(); |
114 |
NbTestCase.deleteSubFiles(ud); |
115 |
|
116 |
System.setProperty("netbeans.user", ud.getPath()); |
117 |
|
118 |
TreeSet<String> modules = new TreeSet<String>(); |
119 |
modules.addAll(findEnabledModules(NbTestSuite.class.getClassLoader())); |
120 |
modules.add("org.openide.filesystems"); |
121 |
modules.add("org.openide.modules"); |
122 |
modules.add("org.openide.util"); |
123 |
modules.remove("org.netbeans.insane"); |
124 |
modules.add("org.netbeans.core.startup"); |
125 |
modules.add("org.netbeans.bootstrap"); |
126 |
turnModules(ud, modules, platform); |
127 |
|
128 |
StringBuilder sb = new StringBuilder(); |
129 |
String sep = ""; |
130 |
for (File f : findClusters()) { |
131 |
turnModules(ud, modules, f); |
132 |
sb.append(sep); |
133 |
sb.append(f.getPath()); |
134 |
sep = File.pathSeparator; |
135 |
} |
136 |
System.setProperty("netbeans.dirs", sb.toString()); |
137 |
|
138 |
List<String> args = new ArrayList<String>(); |
139 |
args.add("--nosplash"); |
140 |
m.invoke(null, (Object)args.toArray(new String[0])); |
141 |
|
142 |
ClassLoader global = Lookup.getDefault().lookup(ClassLoader.class); |
143 |
Assert.assertNotNull("Global classloader is initialized", global); |
144 |
|
145 |
URL[] testCP = preparePath(clazz); |
146 |
JunitLoader testLoader = new JunitLoader(testCP, global, NbTestSuite.class.getClassLoader()); |
147 |
Class<?> sndClazz = testLoader.loadClass(clazz.getName()); |
148 |
|
149 |
new NbTestSuite(sndClazz).run(result); |
150 |
} |
151 |
|
152 |
private URL[] preparePath(Class<?>... classes) { |
153 |
Collection<URL> cp = new LinkedHashSet<URL>(); |
154 |
for (Class c : classes) { |
155 |
URL test = c.getProtectionDomain().getCodeSource().getLocation(); |
156 |
Assert.assertNotNull("URL found for " + c, test); |
157 |
cp.add(test); |
158 |
} |
159 |
return cp.toArray(new URL[0]); |
160 |
} |
121 |
} |
161 |
|
122 |
|
162 |
|
123 |
static final class S extends NbTestSuite { |
163 |
private File findPlatform() { |
124 |
private final Class<?> clazz; |
164 |
try { |
125 |
private final String clusterRegExp; |
165 |
File util = new File(Lookup.class.getProtectionDomain().getCodeSource().getLocation().toURI()); |
|
|
166 |
Assert.assertTrue("Util exists: " + util, util.exists()); |
167 |
|
126 |
|
168 |
return util.getParentFile().getParentFile(); |
127 |
public S(Class<?> aClass) { |
169 |
} catch (URISyntaxException ex) { |
128 |
this(aClass, ".*"); |
170 |
Assert.fail("Cannot find utilities JAR"); |
|
|
171 |
return null; |
172 |
} |
129 |
} |
173 |
} |
130 |
|
174 |
|
131 |
public S(Class<?> aClass, String clusterRegExp) { |
175 |
private File[] findClusters() { |
132 |
super(); |
176 |
List<File> clusters = new ArrayList<File>(); |
133 |
this.clazz = aClass; |
177 |
File plat = findPlatform(); |
134 |
this.clusterRegExp = clusterRegExp; |
178 |
|
|
|
179 |
for (File f : plat.getParentFile().listFiles()) { |
180 |
if (f.equals(plat)) { |
181 |
continue; |
182 |
} |
183 |
if (!f.getName().matches(clusterRegExp)) { |
184 |
continue; |
185 |
} |
186 |
File m = new File(new File(f, "config"), "Modules"); |
187 |
if (m.exists()) { |
188 |
clusters.add(f); |
189 |
} |
190 |
} |
135 |
} |
191 |
return clusters.toArray(new File[0]); |
136 |
|
192 |
} |
137 |
@Override |
193 |
|
138 |
public void run(TestResult result) { |
194 |
private static Pattern CODENAME = Pattern.compile("OpenIDE-Module: *([^/$ \n\r]*)[/]?[0-9]*", Pattern.MULTILINE); |
139 |
try { |
195 |
/** Looks for all modules on classpath of given loader and builds |
140 |
runInRuntimeContainer(result); |
196 |
* their list from them. |
141 |
} catch (Exception ex) { |
197 |
*/ |
142 |
result.addError(this, ex); |
198 |
static Set<String> findEnabledModules(ClassLoader loader) throws IOException { |
|
|
199 |
Set<String> cnbs = new TreeSet<String>(); |
200 |
|
201 |
Enumeration<URL> en = loader.getResources("META-INF/MANIFEST.MF"); |
202 |
while (en.hasMoreElements()) { |
203 |
URL url = en.nextElement(); |
204 |
String manifest = asString(url.openStream(), true); |
205 |
Matcher m = CODENAME.matcher(manifest); |
206 |
if (m.find()) { |
207 |
cnbs.add(m.group(1)); |
208 |
} |
143 |
} |
209 |
} |
144 |
} |
210 |
|
145 |
|
211 |
return cnbs; |
146 |
private void runInRuntimeContainer(TestResult result) throws Exception { |
212 |
} |
147 |
File platform = findPlatform(); |
213 |
|
148 |
File[] boot = new File(platform, "lib").listFiles(); |
214 |
private static String asString(InputStream is, boolean close) throws IOException { |
149 |
List<URL> bootCP = new ArrayList<URL>(); |
215 |
byte[] arr = new byte[is.available()]; |
150 |
for (int i = 0; i < boot.length; i++) { |
216 |
int len = is.read(arr); |
151 |
URL u = boot[i].toURL(); |
217 |
if (len != arr.length) { |
152 |
if (u.toExternalForm().endsWith(".jar")) { |
218 |
throw new IOException("Not fully read: " + arr.length + " was " + len); |
153 |
bootCP.add(u); |
219 |
} |
154 |
} |
220 |
if (close) { |
155 |
} |
221 |
is.close(); |
156 |
// loader that does not see our current classloader |
222 |
} |
157 |
ClassLoader parent = ClassLoader.getSystemClassLoader().getParent(); |
223 |
return new String(arr, "UTF-8"); // NOI18N |
158 |
Assert.assertNotNull("Parent", parent); |
224 |
} |
159 |
URLClassLoader loader = new URLClassLoader(bootCP.toArray(new URL[0]), parent); |
225 |
|
160 |
Class<?> main = loader.loadClass("org.netbeans.Main"); // NOI18N |
226 |
private static final class JunitLoader extends URLClassLoader { |
161 |
Assert.assertEquals("Loaded by our classloader", loader, main.getClassLoader()); |
227 |
private final ClassLoader junit; |
162 |
Method m = main.getDeclaredMethod("main", String[].class); // NOI18N |
228 |
|
163 |
|
229 |
public JunitLoader(URL[] urls, ClassLoader parent, ClassLoader junit) { |
164 |
System.setProperty("java.util.logging.config", "-"); |
230 |
super(urls, parent); |
165 |
System.setProperty("netbeans.home", platform.getPath()); |
231 |
this.junit = junit; |
166 |
|
|
|
167 |
File ud = new File(new File(Manager.getWorkDirPath()), "userdir"); |
168 |
ud.mkdirs(); |
169 |
NbTestCase.deleteSubFiles(ud); |
170 |
|
171 |
System.setProperty("netbeans.user", ud.getPath()); |
172 |
|
173 |
TreeSet<String> modules = new TreeSet<String>(); |
174 |
modules.addAll(findEnabledModules(NbTestSuite.class.getClassLoader())); |
175 |
modules.add("org.openide.filesystems"); |
176 |
modules.add("org.openide.modules"); |
177 |
modules.add("org.openide.util"); |
178 |
modules.remove("org.netbeans.insane"); |
179 |
modules.add("org.netbeans.core.startup"); |
180 |
modules.add("org.netbeans.bootstrap"); |
181 |
turnModules(ud, modules, platform); |
182 |
|
183 |
StringBuilder sb = new StringBuilder(); |
184 |
String sep = ""; |
185 |
for (File f : findClusters()) { |
186 |
turnModules(ud, modules, f); |
187 |
sb.append(sep); |
188 |
sb.append(f.getPath()); |
189 |
sep = File.pathSeparator; |
190 |
} |
191 |
System.setProperty("netbeans.dirs", sb.toString()); |
192 |
|
193 |
List<String> args = new ArrayList<String>(); |
194 |
args.add("--nosplash"); |
195 |
m.invoke(null, (Object)args.toArray(new String[0])); |
196 |
|
197 |
ClassLoader global = Lookup.getDefault().lookup(ClassLoader.class); |
198 |
Assert.assertNotNull("Global classloader is initialized", global); |
199 |
|
200 |
URL[] testCP = preparePath(clazz); |
201 |
JunitLoader testLoader = new JunitLoader(testCP, global, NbTestSuite.class.getClassLoader()); |
202 |
Class<?> sndClazz = testLoader.loadClass(clazz.getName()); |
203 |
|
204 |
new NbTestSuite(sndClazz).run(result); |
232 |
} |
205 |
} |
233 |
|
206 |
|
234 |
@Override |
207 |
private URL[] preparePath(Class<?>... classes) { |
235 |
protected Class<?> findClass(String name) throws ClassNotFoundException { |
208 |
Collection<URL> cp = new LinkedHashSet<URL>(); |
236 |
if (isUnit(name)) { |
209 |
for (Class c : classes) { |
237 |
return junit.loadClass(name); |
210 |
URL test = c.getProtectionDomain().getCodeSource().getLocation(); |
|
|
211 |
Assert.assertNotNull("URL found for " + c, test); |
212 |
cp.add(test); |
238 |
} |
213 |
} |
239 |
return super.findClass(name); |
214 |
return cp.toArray(new URL[0]); |
240 |
} |
215 |
} |
241 |
|
216 |
|
242 |
@Override |
217 |
|
243 |
public URL findResource(String name) { |
218 |
private File findPlatform() { |
244 |
if (isUnit(name)) { |
219 |
try { |
245 |
return junit.getResource(name); |
220 |
File util = new File(Lookup.class.getProtectionDomain().getCodeSource().getLocation().toURI()); |
|
|
221 |
Assert.assertTrue("Util exists: " + util, util.exists()); |
222 |
|
223 |
return util.getParentFile().getParentFile(); |
224 |
} catch (URISyntaxException ex) { |
225 |
Assert.fail("Cannot find utilities JAR"); |
226 |
return null; |
246 |
} |
227 |
} |
247 |
return super.findResource(name); |
|
|
248 |
} |
228 |
} |
249 |
|
229 |
|
250 |
@Override |
230 |
private File[] findClusters() { |
251 |
public Enumeration<URL> findResources(String name) throws IOException { |
231 |
List<File> clusters = new ArrayList<File>(); |
252 |
if (isUnit(name)) { |
232 |
File plat = findPlatform(); |
253 |
return junit.getResources(name); |
233 |
|
|
|
234 |
for (File f : plat.getParentFile().listFiles()) { |
235 |
if (f.equals(plat)) { |
236 |
continue; |
237 |
} |
238 |
if (!f.getName().matches(clusterRegExp)) { |
239 |
continue; |
240 |
} |
241 |
File m = new File(new File(f, "config"), "Modules"); |
242 |
if (m.exists()) { |
243 |
clusters.add(f); |
244 |
} |
254 |
} |
245 |
} |
255 |
return super.findResources(name); |
246 |
return clusters.toArray(new File[0]); |
256 |
} |
247 |
} |
257 |
|
248 |
|
258 |
private final boolean isUnit(String res) { |
249 |
private static Pattern CODENAME = Pattern.compile("OpenIDE-Module: *([^/$ \n\r]*)[/]?[0-9]*", Pattern.MULTILINE); |
259 |
if (res.startsWith("junit")) { |
250 |
/** Looks for all modules on classpath of given loader and builds |
260 |
return true; |
251 |
* their list from them. |
|
|
252 |
*/ |
253 |
static Set<String> findEnabledModules(ClassLoader loader) throws IOException { |
254 |
Set<String> cnbs = new TreeSet<String>(); |
255 |
|
256 |
Enumeration<URL> en = loader.getResources("META-INF/MANIFEST.MF"); |
257 |
while (en.hasMoreElements()) { |
258 |
URL url = en.nextElement(); |
259 |
String manifest = asString(url.openStream(), true); |
260 |
Matcher m = CODENAME.matcher(manifest); |
261 |
if (m.find()) { |
262 |
cnbs.add(m.group(1)); |
263 |
} |
261 |
} |
264 |
} |
262 |
if (res.startsWith("org.junit") || res.startsWith("org/junit")) { |
265 |
|
263 |
return true; |
266 |
return cnbs; |
|
|
267 |
} |
268 |
|
269 |
private static String asString(InputStream is, boolean close) throws IOException { |
270 |
byte[] arr = new byte[is.available()]; |
271 |
int len = is.read(arr); |
272 |
if (len != arr.length) { |
273 |
throw new IOException("Not fully read: " + arr.length + " was " + len); |
264 |
} |
274 |
} |
265 |
if (res.startsWith("org.netbeans.junit") || res.startsWith("org/netbeans/junit")) { |
275 |
if (close) { |
266 |
return true; |
276 |
is.close(); |
267 |
} |
277 |
} |
268 |
return false; |
278 |
return new String(arr, "UTF-8"); // NOI18N |
269 |
} |
279 |
} |
270 |
} |
|
|
271 |
|
280 |
|
272 |
private static Pattern ENABLED = Pattern.compile("<param name=[\"']enabled[\"']>([^<]*)</param>", Pattern.MULTILINE); |
281 |
private static final class JunitLoader extends URLClassLoader { |
273 |
|
282 |
private final ClassLoader junit; |
274 |
private static void turnModules(File ud, TreeSet<String> modules, File... clusterDirs) throws IOException { |
283 |
|
275 |
File config = new File(new File(ud, "config"), "Modules"); |
284 |
public JunitLoader(URL[] urls, ClassLoader parent, ClassLoader junit) { |
276 |
config.mkdirs(); |
285 |
super(urls, parent); |
277 |
|
286 |
this.junit = junit; |
278 |
for (File c : clusterDirs) { |
287 |
} |
279 |
File modulesDir = new File(new File(c, "config"), "Modules"); |
288 |
|
280 |
for (File m : modulesDir.listFiles()) { |
289 |
@Override |
281 |
String n = m.getName(); |
290 |
protected Class<?> findClass(String name) throws ClassNotFoundException { |
282 |
if (n.endsWith(".xml")) { |
291 |
if (isUnit(name)) { |
283 |
n = n.substring(0, n.length() - 4); |
292 |
return junit.loadClass(name); |
284 |
} |
293 |
} |
285 |
n = n.replace('-', '.'); |
294 |
return super.findClass(name); |
286 |
|
295 |
} |
287 |
String xml = asString(new FileInputStream(m), true); |
|
|
288 |
Matcher matcherEnabled = ENABLED.matcher(xml); |
289 |
// Matcher matcherEager = EAGER.matcher(xml); |
290 |
|
291 |
boolean enabled = matcherEnabled.find() && "true".equals(matcherEnabled.group(1)); |
292 |
|
293 |
if (modules.contains(n) != enabled) { |
294 |
assert matcherEnabled.groupCount() == 1 : "Groups: " + matcherEnabled.groupCount() + " for:\n" + xml; |
295 |
|
296 |
|
296 |
try { |
297 |
@Override |
297 |
String out = |
298 |
public URL findResource(String name) { |
298 |
xml.substring(0, matcherEnabled.start(1)) + |
299 |
if (isUnit(name)) { |
299 |
(enabled ? "false" : "true") + |
300 |
return junit.getResource(name); |
300 |
xml.substring(matcherEnabled.end(1)); |
301 |
} |
301 |
writeModule(new File(config, m.getName()), out); |
302 |
return super.findResource(name); |
302 |
} catch (IllegalStateException ex) { |
303 |
} |
303 |
throw (IOException)new IOException("Unparseable:\n" + xml).initCause(ex); |
304 |
|
|
|
305 |
@Override |
306 |
public Enumeration<URL> findResources(String name) throws IOException { |
307 |
if (isUnit(name)) { |
308 |
return junit.getResources(name); |
309 |
} |
310 |
return super.findResources(name); |
311 |
} |
312 |
|
313 |
private final boolean isUnit(String res) { |
314 |
if (res.startsWith("junit")) { |
315 |
return true; |
316 |
} |
317 |
if (res.startsWith("org.junit") || res.startsWith("org/junit")) { |
318 |
return true; |
319 |
} |
320 |
if (res.startsWith("org.netbeans.junit") || res.startsWith("org/netbeans/junit")) { |
321 |
return true; |
322 |
} |
323 |
return false; |
324 |
} |
325 |
} |
326 |
|
327 |
private static Pattern ENABLED = Pattern.compile("<param name=[\"']enabled[\"']>([^<]*)</param>", Pattern.MULTILINE); |
328 |
|
329 |
private static void turnModules(File ud, TreeSet<String> modules, File... clusterDirs) throws IOException { |
330 |
File config = new File(new File(ud, "config"), "Modules"); |
331 |
config.mkdirs(); |
332 |
|
333 |
for (File c : clusterDirs) { |
334 |
File modulesDir = new File(new File(c, "config"), "Modules"); |
335 |
for (File m : modulesDir.listFiles()) { |
336 |
String n = m.getName(); |
337 |
if (n.endsWith(".xml")) { |
338 |
n = n.substring(0, n.length() - 4); |
339 |
} |
340 |
n = n.replace('-', '.'); |
341 |
|
342 |
String xml = asString(new FileInputStream(m), true); |
343 |
Matcher matcherEnabled = ENABLED.matcher(xml); |
344 |
// Matcher matcherEager = EAGER.matcher(xml); |
345 |
|
346 |
boolean enabled = matcherEnabled.find() && "true".equals(matcherEnabled.group(1)); |
347 |
|
348 |
if (modules.contains(n) != enabled) { |
349 |
assert matcherEnabled.groupCount() == 1 : "Groups: " + matcherEnabled.groupCount() + " for:\n" + xml; |
350 |
|
351 |
try { |
352 |
String out = |
353 |
xml.substring(0, matcherEnabled.start(1)) + |
354 |
(enabled ? "false" : "true") + |
355 |
xml.substring(matcherEnabled.end(1)); |
356 |
writeModule(new File(config, m.getName()), out); |
357 |
} catch (IllegalStateException ex) { |
358 |
throw (IOException)new IOException("Unparseable:\n" + xml).initCause(ex); |
359 |
} |
304 |
} |
360 |
} |
305 |
} |
361 |
} |
306 |
} |
362 |
} |
307 |
} |
363 |
} |
308 |
} |
|
|
309 |
|
364 |
|
310 |
private static void writeModule(File file, String xml) throws IOException { |
365 |
private static void writeModule(File file, String xml) throws IOException { |
311 |
FileOutputStream os = new FileOutputStream(file); |
366 |
FileOutputStream os = new FileOutputStream(file); |
312 |
os.write(xml.getBytes("UTF-8")); |
367 |
os.write(xml.getBytes("UTF-8")); |
313 |
os.close(); |
368 |
os.close(); |
314 |
} |
369 |
} |
|
|
370 |
} // end of S |
315 |
} |
371 |
} |
316 |
|
|
|