blob: b0fabdfd95b6828536132d7e81a4fbd3cf2d713a [file] [log] [blame]
// Copyright 2018 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.android.aapt2;
import static com.google.common.base.Predicates.not;
import static java.util.stream.Collectors.joining;
import com.android.aapt.Resources;
import com.android.aapt.Resources.Array;
import com.android.aapt.Resources.Attribute.Symbol;
import com.android.aapt.Resources.CompoundValue;
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.Entry;
import com.android.aapt.Resources.FileReference;
import com.android.aapt.Resources.Item;
import com.android.aapt.Resources.Package;
import com.android.aapt.Resources.Plural;
import com.android.aapt.Resources.Reference;
import com.android.aapt.Resources.ResourceTable;
import com.android.aapt.Resources.Style;
import com.android.aapt.Resources.Type;
import com.android.aapt.Resources.Value;
import com.android.aapt.Resources.XmlAttribute;
import com.android.aapt.Resources.XmlElement;
import com.android.aapt.Resources.XmlNamespace;
import com.android.aapt.Resources.XmlNode;
import com.android.resources.ResourceType;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
import com.google.common.xml.XmlEscapers;
import com.google.devtools.build.android.AndroidResourceOutputs.UniqueZipBuilder;
import com.google.protobuf.ByteString;
import com.google.protobuf.ExtensionRegistry;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.annotation.Nullable;
/**
* Provides an interface to an apk in proto format. Since the apk is backed by a zip, it is
* important to close the ProtoApk when done.
*/
public class ProtoApk implements Closeable {
static final Logger logger = Logger.getLogger(ProtoApk.class.getName());
private static final String RESOURCE_TABLE = "resources.pb";
private static final String MANIFEST = "AndroidManifest.xml";
private static final String RES_DIRECTORY = "res";
private final URI uri;
private final FileSystem apkFileSystem;
private ProtoApk(URI uri, FileSystem apkFileSystem) {
this.uri = uri;
this.apkFileSystem = apkFileSystem;
}
/** Reads a ProtoApk from a path and verifies that it is in the expected format. */
public static ProtoApk readFrom(Path apkPath) throws IOException {
final URI uri = URI.create("jar:" + apkPath.toUri());
return readFrom(uri);
}
private static ProtoApk readFrom(URI uri) throws IOException {
final FileSystem apkFileSystem = FileSystems.newFileSystem(uri, ImmutableMap.of());
Preconditions.checkArgument(Files.exists(apkFileSystem.getPath(RESOURCE_TABLE)));
Preconditions.checkArgument(Files.exists(apkFileSystem.getPath(MANIFEST)));
return new ProtoApk(URI.create(uri.getSchemeSpecificPart()), apkFileSystem);
}
/**
* Creates a copy of the current apk.
*
* @param destination Path to the new apk destination.
* @param resourceFilter A filter for determining whether a given resource will be included in the
* copy.
* @return The new ProtoApk.
* @throws IOException when there are issues reading the apk.
*/
public ProtoApk copy(Path destination, BiPredicate<ResourceType, String> resourceFilter)
throws IOException {
final URI dstZipUri = URI.create("jar:" + destination.toUri());
try (final ZipFile srcZip = new ZipFile(uri.getPath());
final UniqueZipBuilder dstZip = UniqueZipBuilder.createFor(destination)) {
final ResourceTable.Builder dstTableBuilder = ResourceTable.newBuilder();
final ResourceTable resourceTable =
ResourceTable.parseFrom(
Files.newInputStream(apkFileSystem.getPath(RESOURCE_TABLE)),
ExtensionRegistry.getEmptyRegistry());
dstTableBuilder.setSourcePool(resourceTable.getSourcePool());
for (Package pkg : resourceTable.getPackageList()) {
Package dstPkg = copyPackage(resourceFilter, dstZip, pkg);
dstTableBuilder.addPackage(dstPkg);
}
dstZip.addEntry(RESOURCE_TABLE, dstTableBuilder.build().toByteArray(), ZipEntry.DEFLATED);
srcZip.stream()
.filter(not(ZipEntry::isDirectory))
.filter(entry -> !entry.getName().startsWith(RES_DIRECTORY + "/"))
.filter(entry -> !entry.getName().equals(RESOURCE_TABLE))
.forEach(
entry -> {
try {
createDirectories(dstZip, apkFileSystem.getPath(entry.getName()).getParent());
try (InputStream srcEntryInputStream = srcZip.getInputStream(entry)) {
byte[] content = ByteStreams.toByteArray(srcEntryInputStream);
dstZip.addEntry(entry, content);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
return readFrom(dstZipUri);
}
/**
* Recursively creates all parent directories in {@code zip} before creating {@code directory},
* similar to {@link Files#createDirectories}.
*/
private static void createDirectories(UniqueZipBuilder zip, @Nullable Path directory)
throws IOException {
if (directory == null) {
return;
}
createDirectories(zip, directory.getParent());
zip.addDirEntry(directory.toString());
}
private Package copyPackage(
BiPredicate<ResourceType, String> resourceFilter, UniqueZipBuilder dstZip, Package pkg)
throws IOException {
Package.Builder dstPkgBuilder = Package.newBuilder(pkg);
dstPkgBuilder.clearType();
for (Resources.Type type : pkg.getTypeList()) {
copyResourceType(resourceFilter, dstZip, dstPkgBuilder, type);
}
return dstPkgBuilder.build();
}
private void copyResourceType(
BiPredicate<ResourceType, String> resourceFilter,
UniqueZipBuilder dstZip,
Package.Builder dstPkgBuilder,
Resources.Type type)
throws IOException {
Type.Builder dstTypeBuilder = Resources.Type.newBuilder(type);
dstTypeBuilder.clearEntry();
ResourceType resourceType = ResourceType.getEnum(type.getName());
for (Entry entry : type.getEntryList()) {
if (resourceFilter.test(resourceType, entry.getName())) {
copyEntry(dstZip, dstTypeBuilder, entry);
}
}
final Resources.Type dstType = dstTypeBuilder.build();
if (!dstType.getEntryList().isEmpty()) {
dstPkgBuilder.addType(dstType);
}
}
private void copyEntry(UniqueZipBuilder dstZip, Type.Builder dstTypeBuilder, Entry entry)
throws IOException {
dstTypeBuilder.addEntry(Entry.newBuilder(entry));
for (ConfigValue configValue : entry.getConfigValueList()) {
if (configValue.hasValue()
&& configValue.getValue().hasItem()
&& configValue.getValue().getItem().hasFile()) {
final String path = configValue.getValue().getItem().getFile().getPath();
final Path apkFileSystemPath = apkFileSystem.getPath(path);
createDirectories(dstZip, apkFileSystemPath.getParent());
byte[] content = Files.readAllBytes(apkFileSystemPath);
dstZip.addEntry(path, content, ZipEntry.STORED);
}
}
}
public XmlNode getManifest() throws IOException {
try (InputStream in = Files.newInputStream(apkFileSystem.getPath(MANIFEST))) {
return XmlNode.parseFrom(in, ExtensionRegistry.getEmptyRegistry());
}
}
/** Copy manifest as xml to an external directory. */
public Path writeManifestAsXmlTo(Path directory) {
try (XmlWriter out = XmlWriter.openNew(Files.createDirectories(directory).resolve(MANIFEST))) {
out.write(getManifest());
return directory.resolve(MANIFEST);
} catch (IOException e) {
throw new ProtoApkException(e);
}
}
/** The apk as path. */
public Path asApkPath() {
return Paths.get(uri);
}
/** Thrown when errors occur during proto apk processing. */
public static class ProtoApkException extends Aapt2Exception {
ProtoApkException(IOException e) {
super(e);
}
}
private static class XmlWriter implements AutoCloseable {
static final ByteString ANGLE_OPEN = ByteString.copyFrom("<".getBytes(StandardCharsets.UTF_8));
static final ByteString SPACE = ByteString.copyFrom(" ".getBytes(StandardCharsets.UTF_8));
static final ByteString ANGLE_CLOSE = ByteString.copyFrom(">".getBytes(StandardCharsets.UTF_8));
static final ByteString FORWARD_SLASH =
ByteString.copyFrom("/".getBytes(StandardCharsets.UTF_8));
static final ByteString XMLNS = ByteString.copyFrom("xmlns:".getBytes(StandardCharsets.UTF_8));
static final ByteString EQUALS = ByteString.copyFrom("=".getBytes(StandardCharsets.UTF_8));
static final ByteString QUOTE = ByteString.copyFrom("\"".getBytes(StandardCharsets.UTF_8));
static final ByteString COLON = ByteString.copyFrom(":".getBytes(StandardCharsets.UTF_8));
private static final ByteString XML_PRELUDE =
ByteString.copyFrom(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>".getBytes(StandardCharsets.UTF_8));
private final OutputStream out;
private final Deque<Map<ByteString, ByteString>> namespaceStack;
static XmlWriter openNew(Path destination) throws IOException {
return new XmlWriter(Files.newOutputStream(destination, StandardOpenOption.CREATE_NEW));
}
private XmlWriter(OutputStream out) {
this.out = out;
this.namespaceStack = new ArrayDeque<>();
}
public void write(XmlNode node) throws IOException {
XML_PRELUDE.writeTo(out);
writeXmlFrom(node);
}
private void writeXmlFrom(XmlNode node) throws IOException {
if (node.hasElement()) {
writeXmlFrom(node.getElement());
} else {
out.write(node.getTextBytes().toByteArray());
}
}
private void writeXmlFrom(XmlElement element) throws IOException {
ANGLE_OPEN.writeTo(out);
if (!element.getNamespaceUriBytes().isEmpty()) {
findNamespacePrefix(element.getNamespaceUriBytes()).writeTo(out);
COLON.writeTo(out);
}
final ByteString name = element.getNameBytes();
name.writeTo(out);
final Map<ByteString, ByteString> namespaces = new LinkedHashMap<>();
for (XmlNamespace namespace : element.getNamespaceDeclarationList()) {
final ByteString prefix = namespace.getPrefixBytes();
SPACE.writeTo(out);
XMLNS.writeTo(out);
prefix.writeTo(out);
EQUALS.writeTo(out);
quote(namespace.getUriBytes());
namespaces.put(namespace.getUriBytes(), prefix);
}
namespaceStack.push(namespaces);
for (XmlAttribute attribute : element.getAttributeList()) {
SPACE.writeTo(out);
if (!attribute.getNamespaceUriBytes().isEmpty()) {
findNamespacePrefix(attribute.getNamespaceUriBytes()).writeTo(out);
COLON.writeTo(out);
}
attribute.getNameBytes().writeTo(out);
EQUALS.writeTo(out);
quote(attribute.getValueBytes());
}
if (element.getChildList().isEmpty()) {
FORWARD_SLASH.writeTo(out);
ANGLE_CLOSE.writeTo(out);
} else {
ANGLE_CLOSE.writeTo(out);
for (XmlNode child : element.getChildList()) {
writeXmlFrom(child);
}
ANGLE_OPEN.writeTo(out);
FORWARD_SLASH.writeTo(out);
if (!element.getNamespaceUriBytes().isEmpty()) {
findNamespacePrefix(element.getNamespaceUriBytes()).writeTo(out);
COLON.writeTo(out);
}
name.writeTo(out);
ANGLE_CLOSE.writeTo(out);
}
namespaceStack.pop();
}
private void quote(ByteString bytes) throws IOException {
QUOTE.writeTo(out);
out.write(
XmlEscapers.xmlAttributeEscaper()
.escape(bytes.toStringUtf8())
.getBytes(StandardCharsets.UTF_8));
QUOTE.writeTo(out);
}
private ByteString findNamespacePrefix(ByteString uri) {
for (Map<ByteString, ByteString> uriToPrefix : namespaceStack) {
if (uriToPrefix.containsKey(uri)) {
return uriToPrefix.get(uri);
}
}
throw new IllegalStateException(
"Unable to find prefix for "
+ uri.toStringUtf8()
+ " in [ "
+ namespaceStack.stream()
.map(Map::keySet)
.flatMap(Set::stream)
.map(ByteString::toString)
.collect(joining(", "))
+ " ]");
}
@Override
public void close() throws IOException {
out.close();
}
}
/** Traverses the resource table and compiled xml resource using the {@link ResourceVisitor}. */
public <T extends ResourceVisitor> T visitResources(T visitor) throws IOException {
// visit manifest
visitXmlResource(apkFileSystem.getPath(MANIFEST), visitor.enteringManifest());
// visit resource table and associated files.
final ResourceTable resourceTable =
ResourceTable.parseFrom(
Files.newInputStream(apkFileSystem.getPath(RESOURCE_TABLE)),
ExtensionRegistry.getEmptyRegistry());
final List<String> sourcePool =
resourceTable.hasSourcePool()
? decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray())
: ImmutableList.of();
for (Package pkg : resourceTable.getPackageList()) {
ResourcePackageVisitor pkgVisitor =
visitor.enteringPackage(pkg.getPackageId().getId(), pkg.getPackageName());
if (pkgVisitor != null) {
for (Resources.Type type : pkg.getTypeList()) {
ResourceTypeVisitor typeVisitor =
pkgVisitor.enteringResourceType(
type.getTypeId().getId(), ResourceType.getEnum(type.getName()));
if (typeVisitor != null) {
for (Entry entry : type.getEntryList()) {
ResourceValueVisitor entryVisitor =
typeVisitor.enteringDeclaration(entry.getName(), entry.getEntryId().getId());
if (entryVisitor != null) {
for (ConfigValue configValue : entry.getConfigValueList()) {
if (configValue.hasValue()) {
visitValue(entryVisitor, configValue.getValue(), sourcePool);
}
}
}
}
}
}
}
}
return visitor;
}
/** Accessor for the underlying URI of the apk. */
public URI asApk() {
return uri.normalize();
}
// TODO(72324748): Centralize duplicated code with AndroidCompiledDataDeserializer.
private static List<String> decodeSourcePool(byte[] bytes) throws UnsupportedEncodingException {
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
int stringCount = byteBuffer.getInt(8);
boolean isUtf8 = (byteBuffer.getInt(16) & (1 << 8)) != 0;
int stringsStart = byteBuffer.getInt(20);
// Position the ByteBuffer after the metadata
byteBuffer.position(28);
List<String> strings = new ArrayList<>();
for (int i = 0; i < stringCount; i++) {
int stringOffset = stringsStart + byteBuffer.getInt();
if (isUtf8) {
int characterCount = byteBuffer.get(stringOffset) & 0xFF;
if ((characterCount & 0x80) != 0) {
characterCount =
((characterCount & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
}
stringOffset += (characterCount >= 0x80 ? 2 : 1);
int length = byteBuffer.get(stringOffset) & 0xFF;
if ((length & 0x80) != 0) {
length = ((length & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF);
}
stringOffset += (length >= 0x80 ? 2 : 1);
strings.add(new String(bytes, stringOffset, length, "UTF8"));
} else {
// TODO(b/148817379): this next block of lines is forming an int with holes in it.
int characterCount = byteBuffer.get(stringOffset) & 0xFFFF;
if ((characterCount & 0x8000) != 0) {
characterCount =
((characterCount & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
}
stringOffset += 2 * (characterCount >= 0x8000 ? 2 : 1);
int length = byteBuffer.get(stringOffset) & 0xFFFF;
if ((length & 0x8000) != 0) {
length = ((length & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF);
}
stringOffset += 2 * (length >= 0x8000 ? 2 : 1);
strings.add(new String(bytes, stringOffset, length, "UTF16"));
}
}
return strings;
}
private void visitValue(ResourceValueVisitor entryVisitor, Value value, List<String> sourcePool) {
if (value.hasSource()) {
entryVisitor.entering(apkFileSystem.getPath(sourcePool.get(value.getSource().getPathIdx())));
}
switch (value.getValueCase()) {
case ITEM:
visitItem(entryVisitor, value.getItem());
break;
case COMPOUND_VALUE:
visitCompoundValue(entryVisitor, value.getCompoundValue());
break;
default:
throw new IllegalStateException(
"Config value does not have a declared value case: " + value);
}
}
private void visitCompoundValue(ResourceValueVisitor entryVisitor, CompoundValue value) {
switch (value.getValueCase()) {
case STYLE:
visitStyle(entryVisitor, value);
break;
case STYLEABLE:
visitStyleable(entryVisitor, value);
break;
case ATTR:
visitAttr(entryVisitor, value);
break;
case ARRAY:
visitArray(entryVisitor, value);
break;
case PLURAL:
visitPlural(entryVisitor, value);
break;
default:
}
}
private void visitPlural(ResourceValueVisitor entryVisitor, CompoundValue value) {
value.getPlural().getEntryList().stream()
.filter(Plural.Entry::hasItem)
.map(Plural.Entry::getItem)
.forEach(
i -> {
switch (i.getValueCase()) {
case FILE:
visitFile(entryVisitor, i.getFile());
break;
case REF:
visitReference(entryVisitor, i.getRef());
break;
default:
}
});
}
private void visitArray(ResourceValueVisitor entryVisitor, CompoundValue value) {
value.getArray().getElementList().stream()
.filter(Array.Element::hasItem)
.map(Array.Element::getItem)
.forEach(
i -> {
switch (i.getValueCase()) {
case FILE:
visitFile(entryVisitor, i.getFile());
break;
case REF:
visitReference(entryVisitor, i.getRef());
break;
default:
}
});
}
private void visitAttr(ResourceValueVisitor entryVisitor, CompoundValue value) {
value.getAttr().getSymbolList().stream()
.filter(Symbol::hasName)
.map(Symbol::getName)
.forEach(name -> visitReference(entryVisitor, name));
}
private void visitStyleable(ResourceValueVisitor entryVisitor, CompoundValue value) {
value.getStyleable().getEntryList().forEach(e -> visitReference(entryVisitor, e.getAttr()));
}
private void visitStyle(ResourceValueVisitor entryVisitor, CompoundValue value) {
final Style style = value.getStyle();
if (style.hasParent()) {
visitReference(entryVisitor, style.getParent());
}
for (Style.Entry entry : style.getEntryList()) {
if (entry.hasItem()) {
visitItem(entryVisitor, entry.getItem());
}
if (entry.hasKey()) {
visitReference(entryVisitor, entry.getKey());
}
}
}
private void visitItem(ResourceValueVisitor entryVisitor, Item item) {
switch (item.getValueCase()) {
case FILE:
visitFile(entryVisitor, item.getFile());
break;
case REF:
visitReference(entryVisitor, item.getRef());
break;
default:
}
}
private void visitFile(ResourceValueVisitor entryVisitor, FileReference file) {
final Path path = apkFileSystem.getPath(file.getPath());
if (file.getType() == FileReference.Type.PROTO_XML) {
visitXmlResource(path, entryVisitor.entering(path));
} else if (file.getType() != FileReference.Type.PNG) {
entryVisitor.acceptOpaqueFileType(path);
}
}
private void visitXmlResource(Path path, ReferenceVisitor visitor) {
if (visitor == null) {
return;
}
try (InputStream in = Files.newInputStream(path)) {
visit(XmlNode.parseFrom(in, ExtensionRegistry.getEmptyRegistry()), visitor);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void visit(XmlNode node, ReferenceVisitor visitor) {
if (node.hasElement()) {
final XmlElement element = node.getElement();
Consumer<XmlNode> visit =
"wearableApp".equals(element.getName())
? n -> visitWearableApp(n, visitor)
: n -> visit(n, visitor);
visitAttributes(visitor, element);
element.getChildList().forEach(visit);
}
}
private void visitAttributes(ReferenceVisitor visitor, XmlElement element) {
for (XmlAttribute attribute : element.getAttributeList()) {
if (attribute.getCompiledItem().hasRef()) {
visitReference(visitor, attribute.getCompiledItem().getRef());
}
if (attribute.getResourceId() != 0) {
visitor.accept(attribute.getResourceId());
}
}
}
// TODO(b/113166518): Remove when wearable apps have real references.
// this doesn't belong in the protoapk, but has to be included for the time being.
private void visitWearableApp(XmlNode node, ReferenceVisitor visitor) {
if (node.hasElement()) {
final XmlElement element = node.getElement();
if ("rawPathResId".equals(element.getName())) {
visitor.accept(
ResourceType.RAW.getName()
+ "/"
+ element.getChildList().stream().map(XmlNode::getText).collect(joining()));
} else {
visitAttributes(visitor, element);
}
element.getChildList().forEach(c -> visitWearableApp(c, visitor));
}
}
private void visitReference(ReferenceVisitor visitor, Reference ref) {
if (ref.getId() != 0) {
logger.finest(
"Visiting ref by id " + ref.getName() + "=" + "0x" + Integer.toHexString(ref.getId()));
visitor.accept(ref.getId());
} else if (!ref.getName().isEmpty()) {
logger.finest("Visiting ref by name " + ref);
visitor.accept(ref.getName());
} else {
logger.finest("Visiting null by name " + ref);
visitor.acceptNullReference();
}
}
@Override
public void close() throws IOException {
apkFileSystem.close();
}
/** Provides an entry point to recording declared and referenced resources in the apk. */
public interface ResourceVisitor {
/** Called when entering the manifest. If null, the manifest is not visited. */
@Nullable
ManifestVisitor enteringManifest();
/** Called when entering a resource package. If null, the package is not visited. */
@Nullable
ResourcePackageVisitor enteringPackage(int pkgId, String packageName);
}
/** Provides a visitor for packages. */
public interface ResourcePackageVisitor {
/** Called when entering the resource types of the package. If null, the type is not visited. */
@Nullable
ResourceTypeVisitor enteringResourceType(int typeId, ResourceType type);
}
/** Visitor for resources types */
public interface ResourceTypeVisitor {
/**
* Called for resource declarations.
*
* @param name The name of the resource.
* @param resourceId The id of the resource, without the package and type.
* @return A visitor for accepting references to other resources from the declared resource. If
* null, the value is not visited.
*/
@Nullable
ResourceValueVisitor enteringDeclaration(String name, int resourceId);
}
/** A manifest specific resource reference visitor. */
public interface ManifestVisitor extends ReferenceVisitor {}
/** General resource reference visitor. */
public interface ResourceValueVisitor extends ReferenceVisitor {
/** Called when entering the source of a value. Maybe called multiple times for each value. */
ReferenceVisitor entering(Path path);
/* Called when a raw resource contains a non-proto xml file type. */
void acceptOpaqueFileType(Path path);
}
/** Role interface for visiting resource references. */
public interface ReferenceVisitor {
/** Called when a reference is defined by name (resourceType/name). */
void accept(String name);
/** Called when a reference is defined by id (full id, with package and type.) */
void accept(int value);
/** Called when a reference is null. */
default void acceptNullReference() {
// pass
}
}
}