| /******************************************************************************* |
| * Copyright (c) 2003, 2006 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.core.runtime.adaptor; |
| |
| import java.io.*; |
| import java.net.URL; |
| import java.util.*; |
| import org.eclipse.core.runtime.internal.adaptor.*; |
| import org.eclipse.osgi.framework.adaptor.FrameworkAdaptor; |
| import org.eclipse.osgi.framework.adaptor.core.*; |
| import org.eclipse.osgi.framework.internal.core.Constants; |
| import org.eclipse.osgi.framework.internal.core.FrameworkProperties; |
| import org.eclipse.osgi.framework.log.FrameworkLogEntry; |
| import org.eclipse.osgi.framework.util.Headers; |
| import org.eclipse.osgi.service.datalocation.Location; |
| import org.eclipse.osgi.service.pluginconversion.PluginConversionException; |
| import org.eclipse.osgi.util.ManifestElement; |
| import org.eclipse.osgi.util.NLS; |
| import org.osgi.framework.BundleException; |
| import org.osgi.framework.Version; |
| |
| /** |
| * The BundleData implementation used Eclipse. |
| * <p> |
| * Clients may extend this class. |
| * </p> |
| * @since 3.1 |
| */ |
| //Maybe for consistency should it be overriden to do nothing. See also EclipseAdaptor.saveMetadataFor(BundleData) |
| public class EclipseBundleData extends AbstractBundleData { |
| /** bundle manifest type unknown */ |
| static public final byte MANIFEST_TYPE_UNKNOWN = PluginConverterImpl.MANIFEST_TYPE_UNKNOWN; |
| /** bundle manifest type bundle (META-INF/MANIFEST.MF) */ |
| static public final byte MANIFEST_TYPE_BUNDLE = PluginConverterImpl.MANIFEST_TYPE_BUNDLE; |
| /** bundle manifest type plugin (plugin.xml) */ |
| static public final byte MANIFEST_TYPE_PLUGIN = PluginConverterImpl.MANIFEST_TYPE_PLUGIN; |
| /** bundle manifest type fragment (fragment.xml) */ |
| static public final byte MANIFEST_TYPE_FRAGMENT = PluginConverterImpl.MANIFEST_TYPE_FRAGMENT; |
| /** bundle manifest type jared bundle */ |
| static public final byte MANIFEST_TYPE_JAR = PluginConverterImpl.MANIFEST_TYPE_JAR; |
| |
| private static String[] libraryVariants = null; |
| |
| /** data to detect modification made in the manifest */ |
| private long manifestTimeStamp = 0; |
| private byte manifestType = MANIFEST_TYPE_UNKNOWN; |
| |
| // URL protocol designations |
| /** The platform protocol */ |
| public static final String PROTOCOL = "platform"; //$NON-NLS-1$ |
| /** The file protocol */ |
| public static final String FILE = "file"; //$NON-NLS-1$ |
| |
| private static final String PROP_CHECK_CONFIG = "osgi.checkConfiguration"; //$NON-NLS-1$ |
| /** the Plugin-Class header */ |
| protected String pluginClass = null; |
| /** Eclipse-AutoStart header */ |
| private boolean autoStart; |
| private String[] autoStartExceptions; |
| /** shortcut to know if a bundle has a buddy */ |
| protected String buddyList; |
| /** shortcut to know if a bundle is a registrant to a registered policy */ |
| protected String registeredBuddyList; |
| /** shortcut to know if the bundle manifest has package info */ |
| protected boolean hasPackageInfo; |
| /** marks the data as dirty */ |
| protected boolean dirty = false; |
| |
| private static String[] buildLibraryVariants() { |
| ArrayList result = new ArrayList(); |
| EclipseEnvironmentInfo info = EclipseEnvironmentInfo.getDefault(); |
| result.add("ws/" + info.getWS() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ |
| result.add("os/" + info.getOS() + "/" + info.getOSArch() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| result.add("os/" + info.getOS() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ |
| String nl = info.getNL(); |
| nl = nl.replace('_', '/'); |
| while (nl.length() > 0) { |
| result.add("nl/" + nl + "/"); //$NON-NLS-1$ //$NON-NLS-2$ |
| int i = nl.lastIndexOf('/'); |
| nl = (i < 0) ? "" : nl.substring(0, i); //$NON-NLS-1$ |
| } |
| result.add(""); //$NON-NLS-1$ |
| return (String[]) result.toArray(new String[result.size()]); |
| } |
| |
| /** |
| * Constructor for EclipseBundleData. |
| * @param adaptor The adaptor for this bundle data |
| * @param id The bundle id for this bundle data |
| */ |
| public EclipseBundleData(AbstractFrameworkAdaptor adaptor, long id) { |
| super(adaptor, id); |
| } |
| |
| /** |
| * Initialize an existing bundle |
| * @throws IOException if any error occurs loading the existing bundle |
| */ |
| public void initializeExistingBundle() throws IOException { |
| createBaseBundleFile(); |
| if (!checkManifestTimeStamp()) { |
| if (getBundleStoreDir().exists()) { |
| /* create .delete */ |
| FileOutputStream out = new FileOutputStream(new File(getBundleStoreDir(), ".delete")); |
| out.close(); |
| } |
| throw new IOException(); |
| } |
| } |
| |
| private boolean checkManifestTimeStamp() { |
| if (!"true".equalsIgnoreCase(FrameworkProperties.getProperty(PROP_CHECK_CONFIG))) //$NON-NLS-1$ |
| return true; |
| |
| if (PluginConverterImpl.getTimeStamp(getBaseFile(), getManifestType()) == getManifestTimeStamp()) { |
| if ((getManifestType() & (MANIFEST_TYPE_JAR | MANIFEST_TYPE_BUNDLE)) != 0) |
| return true; |
| String cacheLocation = FrameworkProperties.getProperty(LocationManager.PROP_MANIFEST_CACHE); |
| Location parentConfiguration = LocationManager.getConfigurationLocation().getParentLocation(); |
| if (parentConfiguration != null) { |
| try { |
| return checkManifestAndParent(cacheLocation, getSymbolicName(), getVersion().toString(), getManifestType()) != null; |
| } catch (BundleException e) { |
| return false; |
| } |
| } |
| File cacheFile = new File(cacheLocation, getSymbolicName() + '_' + getVersion() + ".MF"); //$NON-NLS-1$ |
| if (cacheFile.isFile()) |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the absolute path name of a native library. The VM invokes this |
| * method to locate the native libraries that belong to classes loaded with |
| * this class loader. If this method returns <code>null</code>, the VM |
| * searches the library along the path specified as the <code>java.library.path</code> |
| * property. |
| * |
| * @param libName |
| * the library name |
| * @return the absolute path of the native library |
| */ |
| public String findLibrary(String libName) { |
| // first do the standard OSGi lookup using the native clauses |
| // in the manifest. If that fails, do the legacy Eclipse lookup. |
| String result = super.findLibrary(libName); |
| if (result != null) |
| return result; |
| if (libraryVariants == null) |
| libraryVariants = buildLibraryVariants(); |
| if (libName.length() == 0) |
| return null; |
| if (libName.charAt(0) == '/' || libName.charAt(0) == '\\') |
| libName = libName.substring(1); |
| libName = System.mapLibraryName(libName); |
| |
| // if (DEBUG && DEBUG_SHOW_ACTIONS && debugNative(libName)) |
| // debug("findLibrary(" + libName + ")"); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| return searchVariants(libraryVariants, libName); |
| |
| } |
| |
| private String searchVariants(String[] variants, String path) { |
| for (int i = 0; i < variants.length; i++) { |
| BundleEntry libEntry = baseBundleFile.getEntry(variants[i] + path); |
| if (libEntry == null) { |
| // if (DEBUG && DEBUG_SHOW_FAILURE) |
| // debug("not found " + variants[i] + path); |
| // //$NON-NLS-1$ |
| } else { |
| // if (DEBUG && DEBUG_SHOW_SUCCESS) |
| // debug("found " + path + " as " + |
| // variants[i] + path); //$NON-NLS-1$ //$NON-NLS-2$ |
| File libFile = baseBundleFile.getFile(variants[i] + path); |
| if (libFile == null) |
| return null; |
| // see bug 88697 - HP requires libraries to have executable permissions |
| if (org.eclipse.osgi.service.environment.Constants.OS_HPUX.equals(EclipseEnvironmentInfo.getDefault().getOS())) { |
| try { |
| // use the string array method in case there is a space in the path |
| Runtime.getRuntime().exec(new String[] {"chmod", "755", libFile.getAbsolutePath()}).waitFor(); //$NON-NLS-1$ //$NON-NLS-2$ |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| return libFile.getAbsolutePath(); |
| } |
| } |
| return null; |
| } |
| |
| //TODO Unused method |
| private URL[] getSearchURLs(URL target) { |
| return new URL[] {target}; |
| } |
| |
| public synchronized Dictionary getManifest() throws BundleException { |
| return getManifest(false); |
| } |
| |
| /** |
| * Gets the manifest for this bundle. If this is the first time the manifest is |
| * ever accessed then the manifest is loaded from the manifest file in the bundle; |
| * otherwise the manifest is loaded from cached data. |
| * @param first set to true if this is the first time the manifest is accessed |
| * for this bundle |
| * @return the manifest for this bundle |
| * @throws BundleException if any error occurs loading the manifest |
| */ |
| public synchronized Dictionary getManifest(boolean first) throws BundleException { |
| if (manifest == null) |
| manifest = first ? loadManifest() : new CachedManifest(this); |
| return manifest; |
| } |
| |
| private boolean isComplete(Dictionary manifest) { |
| // a manifest is complete if it has a Bundle-SymbolicName entry... |
| if (manifest.get(org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME) != null) |
| return true; |
| // ...or it does not have a plugin/fragment manifest where to get the other entries from |
| return getEntry(PluginConverterImpl.PLUGIN_MANIFEST) == null && getEntry(PluginConverterImpl.FRAGMENT_MANIFEST) == null; |
| } |
| |
| /** |
| * Loads the bundle manifest from the bundle. |
| * @return the bundle manifest |
| * @throws BundleException if an error occurs loading the bundle manifest |
| */ |
| public synchronized Dictionary loadManifest() throws BundleException { |
| URL url = getEntry(Constants.OSGI_BUNDLE_MANIFEST); |
| if (url != null) { |
| // the bundle has a built-in manifest - we may not have to generate one |
| Dictionary builtIn = loadManifestFrom(url); |
| // if the manifest is not complete, add entries derived from plug-in/fragment manifest |
| if (!isComplete(builtIn)) { |
| Dictionary generatedManifest = generateManifest(builtIn); |
| if (generatedManifest != null) |
| return generatedManifest; |
| } |
| // the manifest is complete or we could not complete it - take it as it is |
| manifestType = MANIFEST_TYPE_BUNDLE; |
| if (getBaseFile().isFile()) { |
| manifestTimeStamp = getBaseFile().lastModified(); |
| manifestType |= MANIFEST_TYPE_JAR; |
| } else |
| manifestTimeStamp = getBaseBundleFile().getEntry(Constants.OSGI_BUNDLE_MANIFEST).getTime(); |
| return builtIn; |
| } |
| Dictionary result = generateManifest(null); |
| if (result == null) |
| throw new BundleException(NLS.bind(EclipseAdaptorMsg.ECLIPSE_DATA_MANIFEST_NOT_FOUND, getLocation())); |
| return result; |
| } |
| |
| private Headers basicCheckManifest(String cacheLocation, String symbolicName, String version, byte inputType) throws BundleException { |
| File currentFile = new File(cacheLocation, symbolicName + '_' + version + ".MF"); //$NON-NLS-1$ |
| if (PluginConverterImpl.upToDate(currentFile, getBaseFile(), inputType)) { |
| try { |
| return Headers.parseManifest(new FileInputStream(currentFile)); |
| } catch (FileNotFoundException e) { |
| // do nothing. |
| } |
| } |
| return null; |
| } |
| |
| private Headers checkManifestAndParent(String cacheLocation, String symbolicName, String version, byte inputType) throws BundleException { |
| Headers result = basicCheckManifest(cacheLocation, symbolicName, version, inputType); |
| if (result != null) |
| return result; |
| |
| Location parentConfiguration = null; |
| if ((parentConfiguration = LocationManager.getConfigurationLocation().getParentLocation()) != null) { |
| result = basicCheckManifest(new File(parentConfiguration.getURL().getFile(), FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME + '/' + LocationManager.MANIFESTS_DIR).toString(), symbolicName, version, inputType); |
| } |
| return result; |
| } |
| |
| private Dictionary generateManifest(Dictionary originalManifest) throws BundleException { |
| String cacheLocation = FrameworkProperties.getProperty(LocationManager.PROP_MANIFEST_CACHE); |
| if (getSymbolicName() != null) { |
| Headers existingHeaders = checkManifestAndParent(cacheLocation, getSymbolicName(), getVersion().toString(), manifestType); |
| if (existingHeaders != null) |
| return existingHeaders; |
| } |
| |
| PluginConverterImpl converter = PluginConverterImpl.getDefault(); |
| |
| Dictionary generatedManifest; |
| try { |
| generatedManifest = converter.convertManifest(getBaseFile(), true, null, true, null); |
| } catch (PluginConversionException pce) { |
| String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_CONVERTER_ERROR_CONVERTING, getBaseFile()); |
| throw new BundleException(message, pce); |
| } |
| |
| //Now we know the symbolicId and the version of the bundle, we check to see if don't have a manifest for it already |
| Version version = Version.parseVersion((String) generatedManifest.get(Constants.BUNDLE_VERSION)); |
| String symbolicName = ManifestElement.parseHeader(org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME, (String) generatedManifest.get(org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME))[0].getValue(); |
| ManifestElement generatedFrom = ManifestElement.parseHeader(PluginConverterImpl.GENERATED_FROM, (String) generatedManifest.get(PluginConverterImpl.GENERATED_FROM))[0]; |
| Headers existingHeaders = checkManifestAndParent(cacheLocation, symbolicName, version.toString(), Byte.parseByte(generatedFrom.getAttribute(PluginConverterImpl.MANIFEST_TYPE_ATTRIBUTE))); |
| //We don't have a manifest. |
| setManifestTimeStamp(Long.parseLong(generatedFrom.getValue())); |
| setManifestType(Byte.parseByte(generatedFrom.getAttribute(PluginConverterImpl.MANIFEST_TYPE_ATTRIBUTE))); |
| if (!adaptor.canWrite() || existingHeaders != null) |
| return existingHeaders; |
| |
| //merge the original manifest with the generated one |
| if (originalManifest != null) { |
| Enumeration keysEnum = originalManifest.keys(); |
| while (keysEnum.hasMoreElements()) { |
| Object key = keysEnum.nextElement(); |
| generatedManifest.put(key, originalManifest.get(key)); |
| } |
| } |
| |
| //write the generated manifest |
| File bundleManifestLocation = new File(cacheLocation, symbolicName + '_' + version.toString() + ".MF"); //$NON-NLS-1$ |
| try { |
| converter.writeManifest(bundleManifestLocation, generatedManifest, true); |
| } catch (Exception e) { |
| //TODO Need to log |
| } |
| return generatedManifest; |
| |
| } |
| |
| private Dictionary loadManifestFrom(URL manifestURL) throws BundleException { |
| try { |
| return Headers.parseManifest(manifestURL.openStream()); |
| } catch (IOException e) { |
| throw new BundleException(NLS.bind(EclipseAdaptorMsg.ECLIPSE_DATA_ERROR_READING_MANIFEST, getLocation()), e); |
| } |
| } |
| |
| protected void loadFromManifest() throws BundleException { |
| getManifest(true); |
| super.loadFromManifest(); |
| // manifest cannot ever be a cached one otherwise the lines below are bogus |
| if (manifest instanceof CachedManifest) |
| throw new IllegalStateException(); |
| pluginClass = (String) manifest.get(EclipseAdaptor.PLUGIN_CLASS); |
| String lazyStart = (String) manifest.get(EclipseAdaptor.ECLIPSE_LAZYSTART); |
| if (lazyStart == null) |
| lazyStart = (String) manifest.get(EclipseAdaptor.ECLIPSE_AUTOSTART); |
| parseAutoStart(lazyStart); |
| buddyList = (String) manifest.get(Constants.BUDDY_LOADER); |
| registeredBuddyList = (String) manifest.get(Constants.REGISTERED_POLICY); |
| hasPackageInfo = hasPackageInfo(getEntry(Constants.OSGI_BUNDLE_MANIFEST)); |
| } |
| |
| // Used to check the bundle manifest file for any package information. |
| // This is used when '.' is on the Bundle-ClassPath to prevent reading |
| // the bundle manifest for pacakge information when loading classes. |
| private boolean hasPackageInfo(URL url) { |
| if (url == null) |
| return false; |
| BufferedReader br = null; |
| try { |
| br = new BufferedReader(new InputStreamReader(url.openStream())); |
| String line; |
| while ((line = br.readLine()) != null) { |
| if (line.startsWith("Specification-Title: ") || line.startsWith("Specification-Version: ") || line.startsWith("Specification-Vendor: ") || line.startsWith("Implementation-Title: ") || line.startsWith("Implementation-Version: ") || line.startsWith("Implementation-Vendor: ")) //$NON-NLS-1$ //$NON-NLS-2$//$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ |
| return true; |
| } |
| } catch (IOException ioe) { |
| // do nothing |
| } finally { |
| if (br != null) |
| try { |
| br.close(); |
| } catch (IOException e) { |
| // do nothing |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the plugin class |
| * @return the plugin class |
| */ |
| public String getPluginClass() { |
| return pluginClass; |
| } |
| |
| /** |
| * Returns the buddy list for this bundle |
| * @return the buddy list for this bundle |
| */ |
| public String getBuddyList() { |
| return buddyList; |
| } |
| |
| /** |
| * Returns the registered buddy list for this bundle |
| * @return the registered buddy list for this bundle |
| */ |
| public String getRegisteredBuddyList() { |
| return registeredBuddyList; |
| } |
| |
| /** |
| * Sets the plugin class |
| * @param value the plugin class |
| */ |
| public void setPluginClass(String value) { |
| pluginClass = value; |
| } |
| |
| /** |
| * Returns the timestamp for the manifest file |
| * @return the timestamp for the manifest file |
| */ |
| public long getManifestTimeStamp() { |
| return manifestTimeStamp; |
| } |
| |
| /** |
| * Sets the manifest timestamp for this bundle |
| * @param stamp the manifest timestamp |
| */ |
| public void setManifestTimeStamp(long stamp) { |
| manifestTimeStamp = stamp; |
| } |
| |
| /** |
| * Returns the manifest type |
| * @return the manifest type |
| */ |
| public byte getManifestType() { |
| return manifestType; |
| } |
| |
| /** |
| * Sets the manifest type |
| * @param manifestType the manifest type |
| */ |
| public void setManifestType(byte manifestType) { |
| this.manifestType = manifestType; |
| } |
| |
| /** |
| * Sets the auto start flag |
| * @param value the auto start flag |
| */ |
| public void setAutoStart(boolean value) { |
| autoStart = value; |
| } |
| |
| /** |
| * Checks whether this bundle is auto started for all resource/class loads |
| * @return true if the bundle is auto started; false otherwise |
| */ |
| public boolean isAutoStart() { |
| return autoStart; |
| } |
| |
| /** |
| * Returns the status of this bundle which is persisted on shutdown. For bundles |
| * which are auto started the started state is removed to prevent the bundle from |
| * being started on the next startup. |
| * @return the status of this bundle which is persisted on shutdown |
| */ |
| public int getPersistentStatus() { |
| // omit the active state if necessary |
| return isAutoStartable() ? (~Constants.BUNDLE_STARTED) & getStatus() : getStatus(); |
| } |
| |
| /** |
| * Sets the list of auto start exceptions for this bundle |
| * @param autoStartExceptions the list of auto start exceptions |
| */ |
| public void setAutoStartExceptions(String[] autoStartExceptions) { |
| this.autoStartExceptions = autoStartExceptions; |
| } |
| |
| /** |
| * Returns the auto start exception packages |
| * @return the auto start exception packages |
| */ |
| public String[] getAutoStartExceptions() { |
| return autoStartExceptions; |
| } |
| |
| private void parseAutoStart(String headerValue) { |
| autoStart = false; |
| autoStartExceptions = null; |
| ManifestElement[] allElements = null; |
| try { |
| allElements = ManifestElement.parseHeader(EclipseAdaptor.ECLIPSE_LAZYSTART, headerValue); |
| } catch (BundleException e) { |
| // just use the default settings (no auto activation) |
| String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_CLASSLOADER_CANNOT_GET_HEADERS, getLocation()); |
| EclipseAdaptor.getDefault().getFrameworkLog().log(new FrameworkLogEntry(FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME, FrameworkLogEntry.ERROR, 0, message, 0, e, null)); |
| } |
| //Eclipse-AutoStart not found... |
| if (allElements == null) |
| return; |
| // the single value for this element should be true|false |
| autoStart = "true".equalsIgnoreCase(allElements[0].getValue()); //$NON-NLS-1$ |
| // look for any exceptions (the attribute) to the autoActivate setting |
| String exceptionsValue = allElements[0].getAttribute(EclipseAdaptor.ECLIPSE_LAZYSTART_EXCEPTIONS); |
| if (exceptionsValue == null) |
| return; |
| StringTokenizer tokenizer = new StringTokenizer(exceptionsValue, ","); //$NON-NLS-1$ |
| int numberOfTokens = tokenizer.countTokens(); |
| autoStartExceptions = new String[numberOfTokens]; |
| for (int i = 0; i < numberOfTokens; i++) |
| autoStartExceptions[i] = tokenizer.nextToken().trim(); |
| } |
| |
| /** |
| * Checks whether this bundle is auto started for all resource/class loads or only for a |
| * subset of resource/classloads |
| * @return true if the bundle is auto started; false otherwise |
| */ |
| public boolean isAutoStartable() { |
| return autoStart || (autoStartExceptions != null && autoStartExceptions.length > 0); |
| } |
| |
| /** |
| * Save the bundle data in the data file. |
| * |
| * @throws IOException if a write error occurs. |
| */ |
| public synchronized void save() throws IOException { |
| if (adaptor.canWrite() && isDirty()) { |
| ((EclipseAdaptor) adaptor).saveMetaDataFor(this); |
| this.dirty = false; |
| } |
| } |
| |
| public String toString() { |
| return "BundleData for " + getSymbolicName() + " (" + id + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } |
| |
| public File getParentGenerationDir() { |
| Location parentConfiguration = null; |
| Location currentConfiguration = LocationManager.getConfigurationLocation(); |
| if (currentConfiguration != null && (parentConfiguration = currentConfiguration.getParentLocation()) != null) |
| return new File(parentConfiguration.getURL().getFile(), FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME + '/' + LocationManager.BUNDLES_DIR + '/' + getBundleID() + '/' + getGeneration()); |
| return null; |
| } |
| |
| public void setStartLevel(int startLevel) { |
| super.setStartLevel(startLevel); |
| dirty = true; |
| } |
| |
| public void setStatus(int status) { |
| super.setStatus(status); |
| if (!isAutoStartable()) |
| dirty = true; |
| } |
| |
| public boolean isDirty() { |
| return dirty; |
| } |
| |
| void setDirty (boolean dirty) { |
| this.dirty = dirty; |
| } |
| } |