| /******************************************************************************* |
| * Copyright (c) 2000, 2015 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.jdi.internal; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.jdi.internal.jdwp.JdwpCommandPacket; |
| import org.eclipse.jdi.internal.jdwp.JdwpID; |
| import org.eclipse.jdi.internal.jdwp.JdwpObjectID; |
| import org.eclipse.jdi.internal.jdwp.JdwpReplyPacket; |
| |
| import com.sun.jdi.ArrayType; |
| import com.sun.jdi.ClassNotLoadedException; |
| import com.sun.jdi.Field; |
| import com.sun.jdi.IncompatibleThreadStateException; |
| import com.sun.jdi.InternalException; |
| import com.sun.jdi.InvalidTypeException; |
| import com.sun.jdi.InvocationException; |
| import com.sun.jdi.Method; |
| import com.sun.jdi.ObjectCollectedException; |
| import com.sun.jdi.ObjectReference; |
| import com.sun.jdi.ReferenceType; |
| import com.sun.jdi.ThreadReference; |
| import com.sun.jdi.Type; |
| import com.sun.jdi.VMDisconnectedException; |
| import com.sun.jdi.Value; |
| |
| /** |
| * this class implements the corresponding interfaces declared by the JDI |
| * specification. See the com.sun.jdi package for more information. |
| * |
| */ |
| public class ObjectReferenceImpl extends ValueImpl implements ObjectReference { |
| /** JDWP Tag. */ |
| public static final byte tag = JdwpID.OBJECT_TAG; |
| |
| /** ObjectID of object that corresponds to this reference. */ |
| private JdwpObjectID fObjectID; |
| /** |
| * Cached reference type. This value is safe for caching because the type of |
| * an object never changes. |
| */ |
| private ReferenceType fReferenceType; |
| |
| /** |
| * Creates new ObjectReferenceImpl. |
| */ |
| public ObjectReferenceImpl(VirtualMachineImpl vmImpl, JdwpObjectID objectID) { |
| this("ObjectReference", vmImpl, objectID); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Creates new ObjectReferenceImpl. |
| */ |
| public ObjectReferenceImpl(String description, VirtualMachineImpl vmImpl, |
| JdwpObjectID objectID) { |
| super(description, vmImpl); |
| fObjectID = objectID; |
| } |
| |
| /** |
| * @returns tag. |
| */ |
| @Override |
| public byte getTag() { |
| return tag; |
| } |
| |
| /** |
| * @return Returns Jdwp Object ID. |
| */ |
| public JdwpObjectID getObjectID() { |
| return fObjectID; |
| } |
| |
| /** |
| * Prevents garbage collection for this object. |
| */ |
| @Override |
| public void disableCollection() { |
| initJdwpRequest(); |
| try { |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_DISABLE_COLLECTION, this); |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * Permits garbage collection for this object. |
| */ |
| @Override |
| public void enableCollection() { |
| initJdwpRequest(); |
| try { |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_ENABLE_COLLECTION, this); |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * Inner class used to return monitor info. |
| */ |
| private class MonitorInfo { |
| ThreadReferenceImpl owner; |
| int entryCount; |
| ArrayList<ThreadReference> waiters; |
| } |
| |
| /** |
| * @return Returns monitor info. |
| */ |
| private MonitorInfo monitorInfo() throws IncompatibleThreadStateException { |
| if (!virtualMachine().canGetMonitorInfo()) { |
| throw new UnsupportedOperationException(); |
| } |
| // Note that this information should not be cached. |
| initJdwpRequest(); |
| try { |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_MONITOR_INFO, this); |
| switch (replyPacket.errorCode()) { |
| case JdwpReplyPacket.INVALID_THREAD: |
| throw new IncompatibleThreadStateException(); |
| case JdwpReplyPacket.THREAD_NOT_SUSPENDED: |
| throw new IncompatibleThreadStateException(); |
| } |
| |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| |
| DataInputStream replyData = replyPacket.dataInStream(); |
| MonitorInfo result = new MonitorInfo(); |
| result.owner = ThreadReferenceImpl.read(this, replyData); |
| result.entryCount = readInt("entry count", replyData); //$NON-NLS-1$ |
| int nrOfWaiters = readInt("nr of waiters", replyData); //$NON-NLS-1$ |
| result.waiters = new ArrayList<>(nrOfWaiters); |
| for (int i = 0; i < nrOfWaiters; i++) |
| result.waiters.add(ThreadReferenceImpl.read(this, replyData)); |
| return result; |
| } catch (IOException e) { |
| defaultIOExceptionHandler(e); |
| return null; |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * @return Returns an ThreadReference for the thread, if any, which |
| * currently owns this object's monitor. |
| */ |
| @Override |
| public ThreadReference owningThread() |
| throws IncompatibleThreadStateException { |
| return monitorInfo().owner; |
| } |
| |
| /** |
| * @return Returns the number times this object's monitor has been entered |
| * by the current owning thread. |
| */ |
| @Override |
| public int entryCount() throws IncompatibleThreadStateException { |
| return monitorInfo().entryCount; |
| } |
| |
| /** |
| * @return Returns a List containing a ThreadReference for each thread |
| * currently waiting for this object's monitor. |
| */ |
| @Override |
| public List<ThreadReference> waitingThreads() throws IncompatibleThreadStateException { |
| return monitorInfo().waiters; |
| } |
| |
| /** |
| * @return Returns the value of a given instance or static field in this |
| * object. |
| */ |
| @Override |
| public Value getValue(Field field) { |
| ArrayList<Field> list = new ArrayList<>(1); |
| list.add(field); |
| return getValues(list).get(field); |
| } |
| |
| /** |
| * @return Returns objects that directly reference this object. Only objects |
| * that are reachable for the purposes of garbage collection are |
| * returned. Note that an object can also be referenced in other |
| * ways, such as from a local variable in a stack frame, or from a |
| * JNI global reference. Such non-object referrers are not returned |
| * by this method. |
| * |
| * @since 3.3 |
| */ |
| @Override |
| public List<ObjectReference> referringObjects(long maxReferrers) |
| throws UnsupportedOperationException, IllegalArgumentException { |
| try { |
| int max = (int) maxReferrers; |
| if (maxReferrers >= Integer.MAX_VALUE) { |
| max = Integer.MAX_VALUE; |
| } |
| ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); |
| DataOutputStream outData = new DataOutputStream(outBytes); |
| this.getObjectID().write(outData); |
| writeInt(max, "max referrers", outData); //$NON-NLS-1$ |
| |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_REFERRING_OBJECTS, outBytes); |
| switch (replyPacket.errorCode()) { |
| case JdwpReplyPacket.NOT_IMPLEMENTED: |
| throw new UnsupportedOperationException( |
| JDIMessages.ReferenceTypeImpl_27); |
| case JdwpReplyPacket.ILLEGAL_ARGUMENT: |
| throw new IllegalArgumentException( |
| JDIMessages.ReferenceTypeImpl_26); |
| case JdwpReplyPacket.INVALID_OBJECT: |
| throw new ObjectCollectedException( |
| JDIMessages.ObjectReferenceImpl_object_not_known); |
| case JdwpReplyPacket.VM_DEAD: |
| throw new VMDisconnectedException(JDIMessages.vm_dead); |
| } |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| |
| DataInputStream replyData = replyPacket.dataInStream(); |
| int elements = readInt("elements", replyData); //$NON-NLS-1$ |
| if (max > 0 && elements > max) { |
| elements = max; |
| } |
| ArrayList<ObjectReference> list = new ArrayList<>(); |
| for (int i = 0; i < elements; i++) { |
| list.add((ObjectReference)ValueImpl.readWithTag(this, replyData)); |
| } |
| return list; |
| } catch (IOException e) { |
| defaultIOExceptionHandler(e); |
| return null; |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * @return Returns the value of multiple instance and/or static fields in |
| * this object. |
| */ |
| @Override |
| public Map<Field, Value> getValues(List<? extends Field> allFields) { |
| // if the field list is empty, nothing to do. |
| if (allFields.isEmpty()) { |
| return new HashMap<>(); |
| } |
| // Note that this information should not be cached. |
| initJdwpRequest(); |
| try { |
| ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); |
| DataOutputStream outData = new DataOutputStream(outBytes); |
| |
| /* |
| * Distinguish static fields from non-static fields: For static |
| * fields ReferenceTypeImpl.getValues() must be used. |
| */ |
| List<Field> staticFields = new ArrayList<>(); |
| List<FieldImpl> nonStaticFields = new ArrayList<>(); |
| |
| // Separate static and non-static fields. |
| int allFieldsSize = allFields.size(); |
| for (int i = 0; i < allFieldsSize; i++) { |
| FieldImpl field = (FieldImpl) allFields.get(i); |
| checkVM(field); |
| if (field.isStatic()) |
| staticFields.add(field); |
| else |
| nonStaticFields.add(field); |
| } |
| |
| // First get values for the static fields. |
| Map<Field, Value> resultMap; |
| if (staticFields.isEmpty()) { |
| resultMap = new HashMap<>(); |
| } else { |
| resultMap = referenceType().getValues(staticFields); |
| } |
| |
| // if no non-static fields are requested, return directly the |
| // result. |
| if (nonStaticFields.isEmpty()) { |
| return resultMap; |
| } |
| // Then get the values for the non-static fields. |
| int nonStaticFieldsSize = nonStaticFields.size(); |
| write(this, outData); |
| writeInt(nonStaticFieldsSize, "size", outData); //$NON-NLS-1$ |
| for (int i = 0; i < nonStaticFieldsSize; i++) { |
| FieldImpl field = nonStaticFields.get(i); |
| field.write(this, outData); |
| } |
| |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_GET_VALUES, outBytes); |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| |
| DataInputStream replyData = replyPacket.dataInStream(); |
| int nrOfElements = readInt("elements", replyData); //$NON-NLS-1$ |
| if (nrOfElements != nonStaticFieldsSize) |
| throw new InternalError( |
| JDIMessages.ObjectReferenceImpl_Retrieved_a_different_number_of_values_from_the_VM_than_requested_1); |
| |
| for (int i = 0; i < nrOfElements; i++) { |
| resultMap.put(nonStaticFields.get(i), |
| ValueImpl.readWithTag(this, replyData)); |
| } |
| return resultMap; |
| } catch (IOException e) { |
| defaultIOExceptionHandler(e); |
| return null; |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * @return Returns the hash code value. |
| */ |
| @Override |
| public int hashCode() { |
| return fObjectID.hashCode(); |
| } |
| |
| /** |
| * @return Returns true if two mirrors refer to the same entity in the |
| * target VM. |
| * @see java.lang.Object#equals(Object) |
| */ |
| @Override |
| public boolean equals(Object object) { |
| |
| return object != null |
| && object.getClass().equals(this.getClass()) |
| && fObjectID.equals(((ObjectReferenceImpl) object).fObjectID) |
| && virtualMachine().equals( |
| ((MirrorImpl) object).virtualMachine()); |
| } |
| |
| /** |
| * @return Returns Jdwp version of given options. |
| */ |
| private int optionsToJdwpOptions(int options) { |
| int jdwpOptions = 0; |
| if ((options & INVOKE_SINGLE_THREADED) != 0) { |
| jdwpOptions |= MethodImpl.INVOKE_SINGLE_THREADED_JDWP; |
| } |
| if ((options & INVOKE_NONVIRTUAL) != 0) { |
| jdwpOptions |= MethodImpl.INVOKE_NONVIRTUAL_JDWP; |
| } |
| return jdwpOptions; |
| } |
| |
| /** |
| * Invokes the specified static Method in the target VM. |
| * |
| * @return Returns a Value mirror of the invoked method's return value. |
| */ |
| @Override |
| public Value invokeMethod(ThreadReference thread, Method method, List<? extends Value> arguments, int options) throws InvalidTypeException, |
| ClassNotLoadedException, IncompatibleThreadStateException, |
| InvocationException { |
| checkVM(thread); |
| checkVM(method); |
| ThreadReferenceImpl threadImpl = (ThreadReferenceImpl) thread; |
| MethodImpl methodImpl = (MethodImpl) method; |
| |
| // Perform some checks for IllegalArgumentException. |
| if (!isAValidMethod(method)) |
| throw new IllegalArgumentException( |
| JDIMessages.ObjectReferenceImpl_Class_does_not_contain_given_method_2); |
| if (method.argumentTypeNames().size() != arguments.size()) |
| throw new IllegalArgumentException( |
| JDIMessages.ObjectReferenceImpl_Number_of_arguments_doesn__t_match_3); |
| if (method.isConstructor() || method.isStaticInitializer()) |
| throw new IllegalArgumentException( |
| JDIMessages.ObjectReferenceImpl_Method_is_constructor_or_intitializer_4); |
| if ((options & INVOKE_NONVIRTUAL) != 0 && method.isAbstract()) |
| throw new IllegalArgumentException( |
| JDIMessages.ObjectReferenceImpl_Method_is_abstract_and_can_therefore_not_be_invoked_nonvirtual_5); |
| |
| // check the type and the vm of the argument, convert the value if |
| // needed. |
| List<Value> checkedArguments = ValueImpl.checkValues(arguments, method.argumentTypes(), virtualMachineImpl()); |
| |
| initJdwpRequest(); |
| try { |
| ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); |
| DataOutputStream outData = new DataOutputStream(outBytes); |
| write(this, outData); |
| threadImpl.write(this, outData); |
| ((ReferenceTypeImpl) referenceType()).write(this, outData); |
| methodImpl.write(this, outData); |
| |
| writeInt(checkedArguments.size(), "size", outData); //$NON-NLS-1$ |
| Iterator<Value> iter = checkedArguments.iterator(); |
| while (iter.hasNext()) { |
| ValueImpl elt = (ValueImpl) iter.next(); |
| if (elt != null) { |
| elt.writeWithTag(this, outData); |
| } else { |
| ValueImpl.writeNullWithTag(this, outData); |
| } |
| } |
| |
| writeInt(optionsToJdwpOptions(options), |
| "options", MethodImpl.getInvokeOptions(), outData); //$NON-NLS-1$ |
| |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_INVOKE_METHOD, outBytes); |
| switch (replyPacket.errorCode()) { |
| case JdwpReplyPacket.TYPE_MISMATCH: |
| throw new InvalidTypeException(); |
| case JdwpReplyPacket.INVALID_CLASS: |
| throw new ClassNotLoadedException( |
| JDIMessages.ObjectReferenceImpl_One_of_the_arguments_of_ObjectReference_invokeMethod___6); |
| case JdwpReplyPacket.INVALID_THREAD: |
| throw new IncompatibleThreadStateException(); |
| case JdwpReplyPacket.THREAD_NOT_SUSPENDED: |
| throw new IncompatibleThreadStateException(); |
| case JdwpReplyPacket.INVALID_TYPESTATE: |
| throw new IncompatibleThreadStateException(); |
| } |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| DataInputStream replyData = replyPacket.dataInStream(); |
| ValueImpl value = ValueImpl.readWithTag(this, replyData); |
| ObjectReferenceImpl exception = ObjectReferenceImpl |
| .readObjectRefWithTag(this, replyData); |
| if (exception != null) |
| throw new InvocationException(exception); |
| return value; |
| } catch (IOException e) { |
| defaultIOExceptionHandler(e); |
| return null; |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| private boolean isAValidMethod(Method method) { |
| ReferenceType refType = referenceType(); |
| if (refType instanceof ArrayType) { |
| // if the object is an array, check if the method is declared in |
| // java.lang.Object |
| return "java.lang.Object".equals(method.declaringType().name()); //$NON-NLS-1$ |
| } |
| return refType.allMethods().contains(method); |
| } |
| |
| /** |
| * @return Returns if this object has been garbage collected in the target |
| * VM. |
| */ |
| @Override |
| public boolean isCollected() { |
| // Note that this information should not be cached. |
| initJdwpRequest(); |
| try { |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_IS_COLLECTED, this); |
| switch (replyPacket.errorCode()) { |
| case JdwpReplyPacket.INVALID_OBJECT: |
| return true; |
| case JdwpReplyPacket.NOT_IMPLEMENTED: |
| // Workaround for problem in J2ME WTK (wireless toolkit) |
| // @see Bug 12966 |
| try { |
| referenceType(); |
| } catch (ObjectCollectedException e) { |
| return true; |
| } |
| return false; |
| default: |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| break; |
| } |
| DataInputStream replyData = replyPacket.dataInStream(); |
| boolean result = readBoolean("is collected", replyData); //$NON-NLS-1$ |
| return result; |
| } catch (IOException e) { |
| defaultIOExceptionHandler(e); |
| return false; |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * @return Returns the ReferenceType that mirrors the type of this object. |
| */ |
| @Override |
| public ReferenceType referenceType() { |
| if (fReferenceType != null) { |
| return fReferenceType; |
| } |
| initJdwpRequest(); |
| try { |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_REFERENCE_TYPE, this); |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| DataInputStream replyData = replyPacket.dataInStream(); |
| fReferenceType = ReferenceTypeImpl.readWithTypeTag(this, replyData); |
| return fReferenceType; |
| } catch (IOException e) { |
| defaultIOExceptionHandler(e); |
| return null; |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * @return Returns the Type that mirrors the type of this object. |
| */ |
| @Override |
| public Type type() { |
| return referenceType(); |
| } |
| |
| /** |
| * Sets the value of a given instance or static field in this object. |
| */ |
| @Override |
| public void setValue(Field field, Value value) throws InvalidTypeException, |
| ClassNotLoadedException { |
| // Note that this information should not be cached. |
| initJdwpRequest(); |
| try { |
| ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); |
| DataOutputStream outData = new DataOutputStream(outBytes); |
| write(this, outData); |
| writeInt(1, "size", outData); // We only set one field //$NON-NLS-1$ |
| checkVM(field); |
| ((FieldImpl) field).write(this, outData); |
| |
| // check the type and the vm of the value. Convert the value if |
| // needed |
| ValueImpl checkedValue = ValueImpl.checkValue(value, field.type(), |
| virtualMachineImpl()); |
| |
| if (checkedValue != null) { |
| checkedValue.write(this, outData); |
| } else { |
| ValueImpl.writeNull(this, outData); |
| } |
| |
| JdwpReplyPacket replyPacket = requestVM( |
| JdwpCommandPacket.OR_SET_VALUES, outBytes); |
| switch (replyPacket.errorCode()) { |
| case JdwpReplyPacket.TYPE_MISMATCH: |
| throw new InvalidTypeException(); |
| case JdwpReplyPacket.INVALID_CLASS: |
| throw new ClassNotLoadedException(referenceType().name()); |
| } |
| defaultReplyErrorHandler(replyPacket.errorCode()); |
| } catch (IOException e) { |
| defaultIOExceptionHandler(e); |
| } finally { |
| handledJdwpRequest(); |
| } |
| } |
| |
| /** |
| * @return Returns a unique identifier for this ObjectReference. |
| */ |
| @Override |
| public long uniqueID() { |
| return fObjectID.value(); |
| } |
| |
| /** |
| * @return Returns string with value of ID. |
| */ |
| public String idString() { |
| return "(id=" + fObjectID + ")"; //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| /** |
| * @return Returns description of Mirror object. |
| */ |
| @Override |
| public String toString() { |
| try { |
| return type().toString() + " " + idString(); //$NON-NLS-1$ |
| } catch (ObjectCollectedException e) { |
| return JDIMessages.ObjectReferenceImpl__Garbage_Collected__ObjectReference__8 |
| + idString(); |
| } catch (Exception e) { |
| return fDescription; |
| } |
| } |
| |
| /** |
| * @return Reads JDWP representation and returns new instance. |
| */ |
| public static ObjectReferenceImpl readObjectRefWithoutTag( |
| MirrorImpl target, DataInputStream in) throws IOException { |
| VirtualMachineImpl vmImpl = target.virtualMachineImpl(); |
| JdwpObjectID ID = new JdwpObjectID(vmImpl); |
| ID.read(in); |
| if (target.fVerboseWriter != null) |
| target.fVerboseWriter.println("objectReference", ID.value()); //$NON-NLS-1$ |
| |
| if (ID.isNull()) |
| return null; |
| |
| ObjectReferenceImpl mirror = new ObjectReferenceImpl(vmImpl, ID); |
| return mirror; |
| } |
| |
| /** |
| * @return Reads JDWP representation and returns new instance. |
| */ |
| public static ObjectReferenceImpl readObjectRefWithTag(MirrorImpl target, |
| DataInputStream in) throws IOException { |
| byte objectTag = target.readByte("object tag", JdwpID.tagMap(), in); //$NON-NLS-1$ |
| switch (objectTag) { |
| case 0: |
| return null; |
| case ObjectReferenceImpl.tag: |
| return ObjectReferenceImpl.readObjectRefWithoutTag(target, in); |
| case ArrayReferenceImpl.tag: |
| return ArrayReferenceImpl.read(target, in); |
| case ClassLoaderReferenceImpl.tag: |
| return ClassLoaderReferenceImpl.read(target, in); |
| case ClassObjectReferenceImpl.tag: |
| return ClassObjectReferenceImpl.read(target, in); |
| case StringReferenceImpl.tag: |
| return StringReferenceImpl.read(target, in); |
| case ThreadGroupReferenceImpl.tag: |
| return ThreadGroupReferenceImpl.read(target, in); |
| case ThreadReferenceImpl.tag: |
| return ThreadReferenceImpl.read(target, in); |
| } |
| throw new InternalException( |
| JDIMessages.ObjectReferenceImpl_Invalid_ObjectID_tag_encountered___9 |
| + objectTag); |
| } |
| |
| /** |
| * Writes JDWP representation without tag. |
| */ |
| @Override |
| public void write(MirrorImpl target, DataOutputStream out) |
| throws IOException { |
| fObjectID.write(out); |
| if (target.fVerboseWriter != null) |
| target.fVerboseWriter.println("objectReference", fObjectID.value()); //$NON-NLS-1$ |
| } |
| } |