You can customize the code that the Slice-to-Java compiler produces by annotating your Slice definitions with metadata (see
Section 4.17). This section describes how metadata influences several aspects of the generated Java code.
By default, the scope of a Slice definition determines the package of its mapped Java construct. A Slice type defined in a module hierarchy is mapped to a type residing in the equivalent Java package (see
Section 10.4 for more information on the module mapping).
There are times when applications require greater control over the packaging of generated Java classes. For instance, a company may have software development guidelines that require all Java classes to reside in a designated package. One way to satisfy this requirement is to modify the Slice module hierarchy so that the generated code uses the required package by default. In the example below, we have enclosed the original definition of
Workflow::Document in the modules
com::acme so that the compiler will create the class in the
com.acme package:
module com {
module acme {
module Workflow {
class Document {
// ...
};
};
};
};
The Slice-to-Java compiler provides a better way to control the packages of generated code through the use of global metadata (see
Section 4.17). The example above can be converted as follows:
The global metadata directive java:package:com.acme instructs the compiler to generate all of the classes resulting from definitions in this Slice file into the Java package
com.acme. The net effect is the same: the class for
Document is generated in the package
com.acme.Workflow. However, we have addressed the two shortcomings of the first solution by reducing our impact on the interface specification: the Slice-to-Java compiler recognizes the package metadata directive and modifies its actions accordingly, whereas the compilers for other language mappings simply ignore it.
Using global metadata to alter the default package of generated classes has ramifications for the Ice run time when unmarshaling exceptions and concrete class types. The Ice run time dynamically loads generated classes by translating their Slice type ids into Java class names. For example, the Ice run time translates the Slice type id
::Workflow::Document into the class name
Workflow.Document.
However, when the generated classes are placed in a user-specified package, the Ice run time can no longer rely on the direct translation of a Slice type id into a Java class name, and therefore must be configured in order to successfully locate the generated classes. Two configuration properties are supported:
For backward compatibility with Java2, Ice supports an alternate mapping that you must enable via global metadata and use in conjunction with a Java2-specific distribution of Ice for Java.
The global metadata java:java2 activates the Slice-to-Java compiler’s support for the Java2 mapping. The effects of this metadata are described in the sections below. If you would prefer not to modify each of your Slice files to include the metadata, you can use the compiler’s
‑‑meta option instead:
$ slice2java ‑‑meta java:java2 Document.ice
The Java2 mapping cannot be used selectively. The metadata may only appear as global metadata, and has no effect when specified on individual Slice definitions. Furthermore, since the Java2 mapping alters the default mapping of several Slice types, you must use it for all of your Slice definitions. Using the
‑‑meta option as shown above is equivalent to defining the global metadata in
Document.ice and in any files included directly or indirectly by
Document.ice.
Java2 does not have an enumerated type, so the Slice enumerations are emulated using a Java class: the name of the Slice enumeration becomes the name of the Java class; for each enumerator, the class contains two public final members, one with the same name as the enumerator, and one with the same name as the enumerator with a prepended underscore. For example:
enum Fruit { Apple, Pear, Orange };
public final class Fruit {
public static final int _Apple = 0;
public static final int _Pear = 1;
public static final int _Orange = 2;
public static final Fruit Apple = new Fruit(_Apple);
public static final Fruit Pear = new Fruit(_Pear);
public static final Fruit Orange = new Fruit(_Orange);
public int
value() {
// ...
}
public static Fruit
convert(int val) {
// ...
}
public static Fruit
convert(String val) {
// ...
}
// ...
}
Fruit favoriteFruit = Fruit.Apple;
Fruit otherFavoriteFruit = Fruit.Orange;
if (favoriteFruit == Fruit.Apple) // Compare with constant
// ...
if (f1 == f2) // Compare two enums
// ...
switch (f2.value()) { // Switch on enum
case Fruit._Apple:
// ...
break;
case Fruit._Pear
// ...
break;
case Fruit._Orange
// ...
break;
}
As you can see, the generated class enables natural use of enumerated values. The int members with a prepended underscore are constants that encode each enumerator; the
Fruit members are preinitialized enumerators that you can use for initialization and comparison.
The value and
convert methods act as an accessor and a modifier, so you can read and write the value of an enumerated variable as an integer. If you are using the
convert method, you must make sure that the passed value is within the range of the enumeration; failure to do so will result in an assertion failure:
Fruit favoriteFruit = Fruit.convert(4); // Assertion failure!
Note that the generated class contains a number of other members, which we have not shown. These members are internal to the Ice run time and you must not use them in your application code (because they may change from release to release).
As for sequences, the Java2 mapping does not create a separate named type for this definition. Instead,
all dictionaries are simply of type
java.util.Map, so we can use a map of employee structures like any other Java map. (Of course, because
java.util.Map is an abstract class, we must use a concrete class, such as
java.util.HashMap for the actual map.) For example:
java.util.Map em = new java.util.HashMap();
Employee e = new Employee();
e.number = 31;
e.firstName = "James";
e.lastName = "Gosling";
em.put(new Long(e.number), e);
Parameters in the Java2 mapping are statically type safe, with one exception: dictionaries map to
java.util.Map, which is simply a mapping from
java.lang.Object to
java.lang.Object. It follows that, if you pass a map between client and server, you must take care to ensure that the pairs of values you insert into the map are of the correct type. For example:
Given a proxy to an Ages object, the client code could look as follows:
AgesPrx ages = ...; // Get proxy...
java.util.HashMap ageTable = new java.util.HashMap();
String name = "Michi Henning";
Long age = new Long(42);
ageTable.put(age, name); // Oops...
ages.put(ageTable); // ClassCastException!
The problem here is that the order of the parameters for ageTable.put is reversed. When the proxy implementation tries to marshal the data, it notices the type mismatch and throws a
ClassCastException.
The formal type is optional; the compiler uses a default value if one is not defined. The instance type must satisfy an
is‑A relationship with the formal type: either the same class is specified for both types, or the instance type must be derived from the formal type.
The Slice-to-Java compiler generates code that uses the formal type for all occurrences of the modified Slice definition except when the generated code must instantiate the type, in which case the compiler uses the instance type instead.
Although the default mapping of a sequence type to a native Java array is efficient and typesafe, it is not always the most convenient representation of your data. To use a different representation, specify the type information in a metadata directive, as shown in the following example:
It is your responsibility to use a type parameter for the Java class (String in the example above) that is the correct mapping for the sequence’s element type.
Thecompiler requires the formal type to implement java.util.List<E>, where
E is the Java mapping of the element type. If you do not specify a formal type, the compiler uses
java.util.List<E> by default. The same is true for the Java2 mapping, in which the formal type must implement
java.util.List.
It is legal to use Java5 classes in your metadata even if the Java2 mapping is enabled. In this situation, we recommend that you always specify an appropriate formal type explicitly because the compiler’s default type,
java.util.List, may cause the Java compiler to emit warnings when you compile the generated code. The example below demonstrates the use of a formal type:
Note that extra care must be taken when defining custom types that contain nested generic types, such as a custom sequence whose element type is also a custom sequence. The Java5 compiler strictly enforces type safety, therefore any compatibility issues in the custom type metadata will be apparent when the generated code is compiled.
The default instance type for a dictionary is java.util.HashMap<K, V>, where
K is the Java mapping of the key type and
V is the Java mapping of the value type. If the semantics of a
HashMap are not suitable for your application, you can specify an alternate type using metadata as shown in the example below:
It is your responsibility to use type parameters for the Java class (String in the example above) that are the correct mappings for the dictionary’s key and value types.
The compiler requires the formal type to implement java.util.Map<K, V>. If you do not specify a formal type, the compiler uses this type by default. In the Java2 mapping, the formal type must implement
java.util.Map.
It is legal to use Java5 classes in your metadata even if the Java2 mapping is enabled. In this situation, we recommend that you always specify an appropriate formal type explicitly because the compiler’s default type,
java.util.Map, may cause the Java compiler to emit warnings when you compile the generated code. The example below demonstrates the use of a formal type:
Note that extra care must be taken when defining dictionary types that contain nested generic types, such as a dictionary whose element type is a custom sequence. The Java5 compiler strictly enforces type safety, therefore any compatibility issues in the custom type metadata will be apparent when the generated code is compiled.
The use of custom types can be problematic if you are developing Ice applications using both Java2 and Java5. One solution is to specify only Java2 types in your metadata, as shown below:
The disadvantage of this lowest-common-denominator approach is that you sacrifice type safety in Java5 applications. Another solution is to maintain two versions of your Slice files, but that would be unwieldy for all but the simplest applications. Fortunately, the Slice compiler supports a special syntax for addressing this situation:
When the compiler encounters a custom type enclosed in braces, it uses the type as given for the Java2 mapping, and appends the appropriate type specifier for the Java5 mapping. Consider the following example:
When translated using the Java2 mapping, the generated code uses java.util.TreeMap as the instance type for
StringMap. In the Java5 mapping, the generated code uses
java.util.TreeMap<String, String> instead.
Defined in this manner, the Slice-to-Java compiler uses java.util.List<String> (the default formal type) for all occurrences of
StringList, and
java.util.LinkedList<String> when it needs to instantiate
StringList.
You may also specify a custom type more selectively by defining metadata for a data member, parameter or return value. For instance, the mapping for the original Slice definition might be sufficient in most situations, but a different mapping is more convenient in particular cases. The example below demonstrates how to override the sequence mapping for the data member of a structure as well as for several operations:
sequence<string> StringSeq;
struct S {
["java:type:java.util.LinkedList<String>"] StringSeq seq;
};
interface I {
["java:type:java.util.ArrayList<String>"] StringSeq
modifiedReturnValue();
void modifiedInParam(
["java:type:java.util.ArrayList<String>"] StringSeq seq);
void modifiedOutParam(
out ["java:type:java.util.ArrayList<String>"]
StringSeq seq);
};
As you might expect, modifying the mapping for an operation’s parameters or return value may require the application to manually convert values from the original mapping to the modified mapping. For example, suppose we want to invoke the
modifiedInParam operation. The signature of its proxy operation is shown below:
void modifiedInParam(java.util.List<String> seq, Ice.Current curr)
The metadata changes the mapping of the seq parameter to
java.util.List, which is the default formal type. If a caller has a
StringSeq value in the original mapping, it must convert the array as shown in the following example:
String[] seq = new String[2];
seq[0] = "hi";
seq[1] = "there";
IPrx proxy = ...;
proxy.modifiedInParam(java.util.Arrays.asList(seq));
Although we specified the instance type java.util.ArrayList<String> for the parameter, we are still able to pass the result of
asList because its return type (
java.util.List<String>) is compatible with the parameter’s formal type declared by the proxy method. In the case of an operation parameter, the instance type is only relevant to a servant implementation, which may need to make assumptions about the actual type of the parameter.
The mapping for an out parameter uses a generated "holder" class to convey the parameter value (see
Section 10.12.2). If you modify the mapping of an
out parameter, as discussed in the previous section, it is possible that the holder class for the parameter’s unmodified type is no longer compatible with the custom type you have specified. The holder class generated for
StringSeq is shown below:
public final class StringSeqHolder
{
public
StringSeqHolder()
{
}
public
StringSeqHolder(String[] value)
{
this.value = value;
}
public String[] value;
}
An out parameter of type
StringSeq would normally map to a proxy method that used
StringSeqHolder to hold the parameter value. When the parameter is modified, as is the case with the
modifiedOutParam operation, the Slice-to-Java compiler cannot use
StringSeqHolder to hold an instance of
java.util.List<String>, because
StringSeqHolder is only appropriate for the default mapping to a native array.
As a result, the compiler handles these situations using instances of the generic class
Ice.Holder<T>, where
T is the parameter’s formal type. Consider the following example:
sequence<string> StringSeq;
interface I {
void modifiedOutParam(
out ["java:type:java.util.ArrayList<String>"]
StringSeq seq);
};
void modifiedOutParam(
Ice.Holder<java.util.List<java.lang.String> > seq,
Ice.Current curr)
The formal type of the parameter is java.util.List<String>, therefore the holder class becomes
Ice.Holder<java.util.List<String>>.
sequence<string> StringSeq;
interface I {
void modifiedOutParam(
out ["java:type:java.util.ArrayList"] StringSeq seq);
};
void modifiedOutParam(Ice.ListHolder seq, Ice.Current curr)
The Ice.ListHolder class holds an instance of
java.util.List. The compiler uses
ListHolder for all modified
out sequence parameters whose formal type is
java.util.List. As a convenience, the compiler uses the class
Ice.ArrayListHolder for a formal type of
java.util.ArrayList, and
Ice.LinkedListHolder for a formal type of
java.util.LinkedList.
In a similar fashion, the compiler uses Ice.MapHolder when it detects an incompatibility for a modified
out dictionary parameter.
Ice.MapHolder holds a value of type
java.util.Map.
For each data member val of type
T, the mapping generates the following methods:
public T getVal();
public void setVal(T v);
Finally, if T is a sequence type with an element type
E, two methods are generated to provide direct access to elements:
public E getVal(int index);
public void setVal(int index, E v);
The Slice-to-Java compiler considers it a fatal error for a JavaBean method of a class data member to conflict with a declared operation of the class. In this situation, you must rename the operation or the data member, or disable the generation of JavaBean methods for the data member in question.
The JavaBean methods are generated for a data member when the member or its enclosing type is annotated with the
java:getset metadata. The following example demonstrates both styles of usage:
sequence<int> IntSeq;
class C {
["java:getset"] int i;
double d;
};
["java:getset"]
struct S {
bool b;
string str;
};
["java:getset"]
exception E {
IntSeq seq;
};
JavaBean methods are generated for all members of struct S and exception
E, but for only one member of class
C. Relevant portions of the generated code are shown below:
public class C extends Ice.ObjectImpl
{
...
public int i;
public int
getI()
{
return i;
}
public void
setI(int _i)
{
i = _i;
}
public double d;
}
public final class S implements java.lang.Cloneable
{
public boolean b;
public boolean
getB()
{
return b;
}
public void
setB(boolean _b)
{
b = _b;
}
public boolean
isB()
{
return b;
}
public String str;
public String
getStr()
{
return str;
}
public void
setStr(String _str)
{
str = _str;
}
...
}
public class E extends Ice.UserException
{
...
public int[] seq;
public int[]
getSeq()
{
return seq;
}
public void
setSeq(int[] _seq)
{
seq = _seq;
}
public int
getSeq(int _index)
{
return seq[_index];
}
public void
setSeq(int _index, int _val)
{
seq[_index] = _val;
}
...
}