View Javadoc
1   /*
2    * Copyright (C) 2007-2012 Argeo GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.argeo.slc.repo.maven;
17  
18  import java.util.ArrayList;
19  import java.util.List;
20  import java.util.Set;
21  import java.util.StringTokenizer;
22  import java.util.TreeSet;
23  
24  import javax.jcr.Credentials;
25  import javax.jcr.Node;
26  import javax.jcr.NodeIterator;
27  import javax.jcr.Repository;
28  import javax.jcr.RepositoryException;
29  import javax.jcr.Session;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.argeo.jcr.JcrMonitor;
34  import org.argeo.jcr.JcrUtils;
35  import org.argeo.slc.SlcException;
36  import org.argeo.slc.SlcNames;
37  import org.argeo.slc.SlcTypes;
38  import org.argeo.slc.repo.ArtifactIndexer;
39  import org.argeo.slc.repo.RepoConstants;
40  import org.argeo.slc.repo.RepoUtils;
41  import org.eclipse.aether.artifact.Artifact;
42  import org.eclipse.aether.artifact.DefaultArtifact;
43  import org.osgi.framework.Version;
44  
45  /**
46   * Generates binaries-, sources- and sdk-version.pom artifacts for a given
47   * group.
48   */
49  public class GenerateBinaries implements Runnable, SlcNames {
50  	private final static Log log = LogFactory.getLog(GenerateBinaries.class);
51  
52  	// Connection info
53  	private Repository repository;
54  	private Credentials credentials;
55  	private String workspace;
56  
57  	// Business info
58  	private String groupId;
59  	private String parentPomCoordinates;
60  	private String version = null;
61  
62  	// Constants
63  	private String artifactBasePath = RepoConstants.DEFAULT_ARTIFACTS_BASE_PATH;
64  	private List<String> excludedSuffixes = new ArrayList<String>();
65  
66  	// Indexes
67  	private Set<Artifact> binaries = new TreeSet<Artifact>(
68  			new ArtifactIdComparator());
69  	private Set<Artifact> sources = new TreeSet<Artifact>(
70  			new ArtifactIdComparator());
71  
72  	// local cache
73  	private ArtifactIndexer artifactIndexer = new ArtifactIndexer();
74  	private Node allArtifactsHighestVersion;
75  
76  	public void run() {
77  		Session session = null;
78  		try {
79  			session = repository.login(credentials, workspace);
80  			Node groupNode = session.getNode(MavenConventionsUtils.groupPath(
81  					artifactBasePath, groupId));
82  			internalPreProcessing(groupNode, null);
83  			internalProcessing(groupNode, null);
84  		} catch (Exception e) {
85  			throw new SlcException("Cannot normalize group " + groupId + " in "
86  					+ workspace, e);
87  		} finally {
88  			JcrUtils.logoutQuietly(session);
89  		}
90  	}
91  
92  	/**
93  	 * Generates binaries-, sources- and sdk-version.pom artifacts for the given
94  	 * version (or the highest of all children version if none is precised).
95  	 * 
96  	 * By default, it includes each latest version of all artifact of this
97  	 * group.
98  	 * 
99  	 * The 3 generated artifacts are then marked as modular distributions and
100 	 * indexed.
101 	 */
102 	public static void processGroupNode(Node groupNode, String version,
103 			JcrMonitor monitor) throws RepositoryException {
104 		// TODO set artifactsBase based on group node
105 		GenerateBinaries gb = new GenerateBinaries();
106 		String groupId = groupNode.getProperty(SlcNames.SLC_GROUP_BASE_ID)
107 				.getString();
108 		gb.setGroupId(groupId);
109 		gb.setVersion(version);
110 		// TODO use already done pre-processing
111 		gb.internalPreProcessing(groupNode, monitor);
112 		gb.internalProcessing(groupNode, monitor);
113 	}
114 
115 	/** Only builds local indexes. Does not change anything in the local Session */
116 	public static GenerateBinaries preProcessGroupNode(Node groupNode,
117 			JcrMonitor monitor) throws RepositoryException {
118 		// TODO set artifactsBase based on group node
119 		GenerateBinaries gb = new GenerateBinaries();
120 		String groupId = groupNode.getProperty(SlcNames.SLC_GROUP_BASE_ID)
121 				.getString();
122 		gb.setGroupId(groupId);
123 		// gb.setVersion(version);
124 		// gb.setOverridePoms(overridePoms);
125 		gb.internalPreProcessing(groupNode, monitor);
126 		return gb;
127 	}
128 
129 	// exposes indexes. to display results of the pre-processing phase.
130 	public Set<Artifact> getBinaries() {
131 		return binaries;
132 	}
133 
134 	public Artifact getHighestArtifactVersion() throws RepositoryException {
135 		return allArtifactsHighestVersion == null ? null : RepoUtils
136 				.asArtifact(allArtifactsHighestVersion);
137 	}
138 
139 	// //////////////////////////////////////
140 	// INTERNAL METHODS
141 
142 	/**
143 	 * Browse all children of a Node considered as a folder that follows Aether
144 	 * conventions i.e that has Aether's artifact base as children.
145 	 * 
146 	 * Each of such child contains a set of Aether artifact versions. This
147 	 * methods build the binaries {@code Set<Artifact>} and other indexes. It
148 	 * does not impact the
149 	 */
150 	protected void internalPreProcessing(Node groupNode, JcrMonitor monitor)
151 			throws RepositoryException {
152 		if (monitor != null)
153 			monitor.subTask("Pre processing group " + groupId);
154 
155 		// Process all direct children nodes,
156 		// gathering latest versions of each artifact
157 		allArtifactsHighestVersion = null;
158 
159 		aBases: for (NodeIterator aBases = groupNode.getNodes(); aBases
160 				.hasNext();) {
161 			Node aBase = aBases.nextNode();
162 			if (aBase.isNodeType(SlcTypes.SLC_ARTIFACT_BASE)) {
163 				Node highestAVersion = getArtifactLatestVersion(aBase);
164 				if (highestAVersion == null)
165 					continue aBases;
166 				else {
167 					// retrieve relevant child node
168 					// Information is stored on the NT_FILE child node.
169 					for (NodeIterator files = highestAVersion.getNodes(); files
170 							.hasNext();) {
171 						Node file = files.nextNode();
172 						if (file.isNodeType(SlcTypes.SLC_BUNDLE_ARTIFACT)) {
173 							if (log.isDebugEnabled())
174 								log.debug("Pre-Processing " + file.getName());
175 							preProcessBundleArtifact(file);
176 						}
177 					}
178 				}
179 			}
180 		}
181 		// if (log.isDebugEnabled()) {
182 		// int bundleCount = symbolicNamesToNodes.size();
183 		// log.debug("" + bundleCount + " bundles have been indexed for "
184 		// + groupId);
185 		// }
186 	}
187 
188 	/** Does the real job : writes JCR META-DATA and generates binaries */
189 	protected void internalProcessing(Node groupNode, JcrMonitor monitor)
190 			throws RepositoryException {
191 		if (monitor != null)
192 			monitor.subTask("Processing group " + groupId);
193 
194 		Session session = groupNode.getSession();
195 
196 		// if version not set or empty, use the highest version
197 		// useful when indexing a product maven repository where
198 		// all artifacts have the same version for a given release
199 		// => the version can then be left empty
200 		if (version == null || version.trim().equals(""))
201 			if (allArtifactsHighestVersion != null)
202 				version = allArtifactsHighestVersion.getProperty(
203 						SLC_ARTIFACT_VERSION).getString();
204 			else
205 				throw new SlcException("Group version " + version
206 						+ " is empty.");
207 
208 		// int bundleCount = symbolicNamesToNodes.size();
209 		// int count = 1;
210 		// for (Node bundleNode : symbolicNamesToNodes.values()) {
211 		// if (log.isDebugEnabled())
212 		// log.debug("Processing " + bundleNode.getName() + " ( " + count
213 		// + "/" + bundleCount + " )");
214 		//
215 		// // processBundleArtifact(bundleNode);
216 		// // bundleNode.getSession().save();
217 		// count++;
218 		// }
219 
220 		// indexes
221 		Set<Artifact> indexes = new TreeSet<Artifact>(
222 				new ArtifactIdComparator());
223 
224 		Artifact indexArtifact;
225 		indexArtifact = writeIndex(session, RepoConstants.BINARIES_ARTIFACT_ID,
226 				binaries);
227 		indexes.add(indexArtifact);
228 
229 		indexArtifact = writeIndex(session, RepoConstants.SOURCES_ARTIFACT_ID,
230 				sources);
231 		indexes.add(indexArtifact);
232 
233 		// sdk
234 		writeIndex(session, RepoConstants.SDK_ARTIFACT_ID, indexes);
235 
236 		if (monitor != null)
237 			monitor.worked(1);
238 	}
239 
240 	protected void preProcessBundleArtifact(Node bundleNode)
241 			throws RepositoryException {
242 
243 		String symbolicName = JcrUtils.get(bundleNode, SLC_SYMBOLIC_NAME);
244 		// Sanity check.
245 		if (symbolicName == null)
246 			log.warn("Symbolic name is null for bundle " + bundleNode);
247 
248 		// Manage source bundles
249 		if (symbolicName.endsWith(".source")) {
250 			// TODO make a shared node with classifier 'sources'?
251 			String bundleName = RepoUtils
252 					.extractBundleNameFromSourceName(symbolicName);
253 			for (String excludedSuffix : excludedSuffixes) {
254 				if (bundleName.endsWith(excludedSuffix))
255 					return;// skip adding to sources
256 			}
257 			sources.add(RepoUtils.asArtifact(bundleNode));
258 			return;
259 		}
260 
261 		// // Build indexes
262 		// NodeIterator exportPackages = bundleNode.getNodes(SLC_
263 		// + Constants.EXPORT_PACKAGE);
264 		// while (exportPackages.hasNext()) {
265 		// Node exportPackage = exportPackages.nextNode();
266 		// String pkg = JcrUtils.get(exportPackage, SLC_NAME);
267 		// packagesToSymbolicNames.put(pkg, symbolicName);
268 		// }
269 		//
270 		// symbolicNamesToNodes.put(symbolicName, bundleNode);
271 		// for (String excludedSuffix : excludedSuffixes) {
272 		// if (symbolicName.endsWith(excludedSuffix))
273 		// return;// skip adding to binaries
274 		// }
275 
276 		binaries.add(RepoUtils.asArtifact(bundleNode));
277 
278 		// Extra check. to remove
279 		if (bundleNode.getSession().hasPendingChanges())
280 			throw new SlcException("Pending changes in the session, "
281 					+ "this should not be true here.");
282 	}
283 
284 	// protected void processBundleArtifact(Node bundleNode)
285 	// throws RepositoryException {
286 	// Node artifactFolder = bundleNode.getParent();
287 	// String baseName = FilenameUtils.getBaseName(bundleNode.getName());
288 	//
289 	// // pom
290 	// String pomName = baseName + ".pom";
291 	// if (artifactFolder.hasNode(pomName) && !overridePoms)
292 	// return;// skip
293 	//
294 	// String pom = generatePomForBundle(bundleNode);
295 	// Node pomNode = JcrUtils.copyBytesAsFile(artifactFolder, pomName,
296 	// pom.getBytes());
297 	// // checksum
298 	// String bundleSha = JcrUtils.checksumFile(bundleNode, "SHA-1");
299 	// JcrUtils.copyBytesAsFile(artifactFolder,
300 	// bundleNode.getName() + ".sha1", bundleSha.getBytes());
301 	// String pomSha = JcrUtils.checksumFile(pomNode, "SHA-1");
302 	// JcrUtils.copyBytesAsFile(artifactFolder, pomNode.getName() + ".sha1",
303 	// pomSha.getBytes());
304 	// }
305 
306 	// ////////////////////
307 	// LOCAL WRITERS
308 	//
309 
310 	private Artifact writeIndex(Session session, String artifactId,
311 			Set<Artifact> artifacts) throws RepositoryException {
312 		Artifact artifact = new DefaultArtifact(groupId, artifactId, "pom",
313 				version);
314 		Artifact parentArtifact = parentPomCoordinates != null ? new DefaultArtifact(
315 				parentPomCoordinates) : null;
316 		String pom = MavenConventionsUtils.artifactsAsDependencyPom(artifact,
317 				artifacts, parentArtifact);
318 		Node node = RepoUtils.copyBytesAsArtifact(
319 				session.getNode(artifactBasePath), artifact, pom.getBytes());
320 		artifactIndexer.index(node);
321 
322 		// TODO factorize
323 		String pomSha = JcrUtils.checksumFile(node, "SHA-1");
324 		JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".sha1",
325 				pomSha.getBytes());
326 		String pomMd5 = JcrUtils.checksumFile(node, "MD5");
327 		JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".md5",
328 				pomMd5.getBytes());
329 		session.save();
330 		return artifact;
331 	}
332 
333 	// Helpers
334 	private Node getArtifactLatestVersion(Node artifactBase) {
335 		try {
336 			Node highestAVersion = null;
337 			for (NodeIterator aVersions = artifactBase.getNodes(); aVersions
338 					.hasNext();) {
339 				Node aVersion = aVersions.nextNode();
340 				if (aVersion.isNodeType(SlcTypes.SLC_ARTIFACT_VERSION_BASE)) {
341 					if (highestAVersion == null) {
342 						highestAVersion = aVersion;
343 						if (allArtifactsHighestVersion == null)
344 							allArtifactsHighestVersion = aVersion;
345 						// Correctly handle following arrival order:
346 						// Name1 - V1, name2 - V3
347 						else {
348 							Version cachedHighestVersion = extractOsgiVersion(allArtifactsHighestVersion);
349 							Version currVersion = extractOsgiVersion(aVersion);
350 							if (currVersion.compareTo(cachedHighestVersion) > 0)
351 								allArtifactsHighestVersion = aVersion;
352 						}
353 					} else {
354 						Version currVersion = extractOsgiVersion(aVersion);
355 						Version currentHighestVersion = extractOsgiVersion(highestAVersion);
356 						if (currVersion.compareTo(currentHighestVersion) > 0) {
357 							highestAVersion = aVersion;
358 						}
359 						if (currVersion
360 								.compareTo(extractOsgiVersion(allArtifactsHighestVersion)) > 0) {
361 							allArtifactsHighestVersion = aVersion;
362 						}
363 					}
364 
365 				}
366 			}
367 			return highestAVersion;
368 		} catch (RepositoryException re) {
369 			throw new SlcException("Unable to get latest version for node "
370 					+ artifactBase, re);
371 		}
372 	}
373 
374 	private Version extractOsgiVersion(Node artifactVersion)
375 			throws RepositoryException {
376 		String rawVersion = artifactVersion.getProperty(SLC_ARTIFACT_VERSION)
377 				.getString();
378 		String cleanVersion = rawVersion.replace("-SNAPSHOT", ".SNAPSHOT");
379 		Version osgiVersion = null;
380 		// log invalid version value to enable tracking them
381 		try {
382 			osgiVersion = new Version(cleanVersion);
383 		} catch (IllegalArgumentException e) {
384 			log.error("Version string " + cleanVersion + " is invalid ");
385 			String twickedVersion = twickInvalidVersion(cleanVersion);
386 			osgiVersion = new Version(twickedVersion);
387 			log.error("Using " + twickedVersion + " instead");
388 			// throw e;
389 		}
390 		return osgiVersion;
391 	}
392 
393 	private String twickInvalidVersion(String tmpVersion) {
394 		String[] tokens = tmpVersion.split("\\.");
395 		if (tokens.length == 3 && tokens[2].lastIndexOf("-") > 0) {
396 			String newSuffix = tokens[2].replaceFirst("-", ".");
397 			tmpVersion = tmpVersion.replaceFirst(tokens[2], newSuffix);
398 		} else if (tokens.length > 4) {
399 			// FIXME manually remove other "."
400 			StringTokenizer st = new StringTokenizer(tmpVersion, ".", true);
401 			StringBuilder builder = new StringBuilder();
402 			// Major
403 			builder.append(st.nextToken()).append(st.nextToken());
404 			// Minor
405 			builder.append(st.nextToken()).append(st.nextToken());
406 			// Micro
407 			builder.append(st.nextToken()).append(st.nextToken());
408 			// Qualifier
409 			builder.append(st.nextToken());
410 			while (st.hasMoreTokens()) {
411 				// consume delimiter
412 				st.nextToken();
413 				if (st.hasMoreTokens())
414 					builder.append("-").append(st.nextToken());
415 			}
416 			tmpVersion = builder.toString();
417 		}
418 		return tmpVersion;
419 	}
420 
421 	// private String generatePomForBundle(Node n) throws RepositoryException {
422 	// String ownSymbolicName = JcrUtils.get(n, SLC_SYMBOLIC_NAME);
423 	//
424 	// StringBuffer p = new StringBuffer();
425 	//
426 	// // XML header
427 	// p.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
428 	// p.append("<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n");
429 	// p.append("<modelVersion>4.0.0</modelVersion>");
430 	//
431 	// // Artifact
432 	// p.append("<groupId>").append(JcrUtils.get(n, SLC_GROUP_ID))
433 	// .append("</groupId>\n");
434 	// p.append("<artifactId>").append(JcrUtils.get(n, SLC_ARTIFACT_ID))
435 	// .append("</artifactId>\n");
436 	// p.append("<version>").append(JcrUtils.get(n, SLC_ARTIFACT_VERSION))
437 	// .append("</version>\n");
438 	// p.append("<packaging>pom</packaging>\n");
439 	// if (n.hasProperty(SLC_ + Constants.BUNDLE_NAME))
440 	// p.append("<name>")
441 	// .append(JcrUtils.get(n, SLC_ + Constants.BUNDLE_NAME))
442 	// .append("</name>\n");
443 	// if (n.hasProperty(SLC_ + Constants.BUNDLE_DESCRIPTION))
444 	// p.append("<description>")
445 	// .append(JcrUtils
446 	// .get(n, SLC_ + Constants.BUNDLE_DESCRIPTION))
447 	// .append("</description>\n");
448 	//
449 	// // Dependencies
450 	// Set<String> dependenciesSymbolicNames = new TreeSet<String>();
451 	// Set<String> optionalSymbolicNames = new TreeSet<String>();
452 	// NodeIterator importPackages = n.getNodes(SLC_
453 	// + Constants.IMPORT_PACKAGE);
454 	// while (importPackages.hasNext()) {
455 	// Node importPackage = importPackages.nextNode();
456 	// String pkg = JcrUtils.get(importPackage, SLC_NAME);
457 	// if (packagesToSymbolicNames.containsKey(pkg)) {
458 	// String dependencySymbolicName = packagesToSymbolicNames
459 	// .get(pkg);
460 	// if (JcrUtils.check(importPackage, SLC_OPTIONAL))
461 	// optionalSymbolicNames.add(dependencySymbolicName);
462 	// else
463 	// dependenciesSymbolicNames.add(dependencySymbolicName);
464 	// } else {
465 	// if (!JcrUtils.check(importPackage, SLC_OPTIONAL)
466 	// && !systemPackages.contains(pkg))
467 	// log.warn("No bundle found for pkg " + pkg);
468 	// }
469 	// }
470 	//
471 	// if (n.hasNode(SLC_ + Constants.FRAGMENT_HOST)) {
472 	// String fragmentHost = JcrUtils.get(
473 	// n.getNode(SLC_ + Constants.FRAGMENT_HOST),
474 	// SLC_SYMBOLIC_NAME);
475 	// dependenciesSymbolicNames.add(fragmentHost);
476 	// }
477 	//
478 	// // TODO require bundles
479 	//
480 	// List<Node> dependencyNodes = new ArrayList<Node>();
481 	// for (String depSymbName : dependenciesSymbolicNames) {
482 	// if (depSymbName.equals(ownSymbolicName))
483 	// continue;// skip self
484 	//
485 	// if (symbolicNamesToNodes.containsKey(depSymbName))
486 	// dependencyNodes.add(symbolicNamesToNodes.get(depSymbName));
487 	// else
488 	// log.warn("Could not find node for " + depSymbName);
489 	// }
490 	// List<Node> optionalDependencyNodes = new ArrayList<Node>();
491 	// for (String depSymbName : optionalSymbolicNames) {
492 	// if (symbolicNamesToNodes.containsKey(depSymbName))
493 	// optionalDependencyNodes.add(symbolicNamesToNodes
494 	// .get(depSymbName));
495 	// else
496 	// log.warn("Could not find node for " + depSymbName);
497 	// }
498 	//
499 	// p.append("<dependencies>\n");
500 	// for (Node dependencyNode : dependencyNodes) {
501 	// p.append("<dependency>\n");
502 	// p.append("\t<groupId>")
503 	// .append(JcrUtils.get(dependencyNode, SLC_GROUP_ID))
504 	// .append("</groupId>\n");
505 	// p.append("\t<artifactId>")
506 	// .append(JcrUtils.get(dependencyNode, SLC_ARTIFACT_ID))
507 	// .append("</artifactId>\n");
508 	// p.append("</dependency>\n");
509 	// }
510 	//
511 	// if (optionalDependencyNodes.size() > 0)
512 	// p.append("<!-- OPTIONAL -->\n");
513 	// for (Node dependencyNode : optionalDependencyNodes) {
514 	// p.append("<dependency>\n");
515 	// p.append("\t<groupId>")
516 	// .append(JcrUtils.get(dependencyNode, SLC_GROUP_ID))
517 	// .append("</groupId>\n");
518 	// p.append("\t<artifactId>")
519 	// .append(JcrUtils.get(dependencyNode, SLC_ARTIFACT_ID))
520 	// .append("</artifactId>\n");
521 	// p.append("\t<optional>true</optional>\n");
522 	// p.append("</dependency>\n");
523 	// }
524 	// p.append("</dependencies>\n");
525 	//
526 	// // Dependency management
527 	// p.append("<dependencyManagement>\n");
528 	// p.append("<dependencies>\n");
529 	// p.append("<dependency>\n");
530 	// p.append("\t<groupId>").append(groupId).append("</groupId>\n");
531 	// p.append("\t<artifactId>")
532 	// .append(ownSymbolicName.endsWith(".source") ?
533 	// RepoConstants.SOURCES_ARTIFACT_ID
534 	// : RepoConstants.BINARIES_ARTIFACT_ID)
535 	// .append("</artifactId>\n");
536 	// p.append("\t<version>").append(version).append("</version>\n");
537 	// p.append("\t<type>pom</type>\n");
538 	// p.append("\t<scope>import</scope>\n");
539 	// p.append("</dependency>\n");
540 	// p.append("</dependencies>\n");
541 	// p.append("</dependencyManagement>\n");
542 	//
543 	// p.append("</project>\n");
544 	// return p.toString();
545 	// }
546 
547 	/* SETTERS */
548 	public void setRepository(Repository repository) {
549 		this.repository = repository;
550 	}
551 
552 	public void setCredentials(Credentials credentials) {
553 		this.credentials = credentials;
554 	}
555 
556 	public void setWorkspace(String workspace) {
557 		this.workspace = workspace;
558 	}
559 
560 	public void setGroupId(String groupId) {
561 		this.groupId = groupId;
562 	}
563 
564 	public void setParentPomCoordinates(String parentPomCoordinates) {
565 		this.parentPomCoordinates = parentPomCoordinates;
566 	}
567 
568 	public void setArtifactBasePath(String artifactBasePath) {
569 		this.artifactBasePath = artifactBasePath;
570 	}
571 
572 	public void setVersion(String version) {
573 		this.version = version;
574 	}
575 
576 	public void setExcludedSuffixes(List<String> excludedSuffixes) {
577 		this.excludedSuffixes = excludedSuffixes;
578 	}
579 
580 	public void setArtifactIndexer(ArtifactIndexer artifactIndexer) {
581 		this.artifactIndexer = artifactIndexer;
582 	}
583 }