Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
it.auties.whatsapp.util.DefaultControllerSerializer Maven / Gradle / Ivy
package it.auties.whatsapp.util;
import it.auties.whatsapp.api.ClientType;
import it.auties.whatsapp.api.TextPreviewSetting;
import it.auties.whatsapp.api.WebHistoryLength;
import it.auties.whatsapp.controller.*;
import it.auties.whatsapp.model.chat.Chat;
import it.auties.whatsapp.model.chat.ChatBuilder;
import it.auties.whatsapp.model.chat.ChatEphemeralTimer;
import it.auties.whatsapp.model.companion.CompanionDevice;
import it.auties.whatsapp.model.info.ContextInfo;
import it.auties.whatsapp.model.jid.Jid;
import it.auties.whatsapp.model.message.model.ContextualMessage;
import it.auties.whatsapp.model.mobile.PhoneNumber;
import it.auties.whatsapp.model.newsletter.Newsletter;
import it.auties.whatsapp.model.signal.auth.UserAgent;
import it.auties.whatsapp.model.signal.keypair.SignalKeyPair;
import it.auties.whatsapp.model.signal.keypair.SignalSignedKeyPair;
import it.auties.whatsapp.model.sync.HistorySyncMessage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class DefaultControllerSerializer implements ControllerSerializer {
private static final Path DEFAULT_SERIALIZER_PATH = Path.of(System.getProperty("user.home") + "/.cobalt/");
private static final String CHAT_PREFIX = "chat_";
private static final String NEWSLETTER_PREFIX = "newsletter_";
private static final String STORE_NAME = "store.smile";
private static final String KEYS_NAME = "keys.smile";
private static final Map serializers = new ConcurrentHashMap<>();
private final Path baseDirectory;
private final ConcurrentMap> attributeStoreSerializers;
private LinkedList cachedUuids;
private LinkedList cachedPhoneNumbers;
static {
serializers.put(DEFAULT_SERIALIZER_PATH, new DefaultControllerSerializer(DEFAULT_SERIALIZER_PATH));
}
public static ControllerSerializer of() {
return Objects.requireNonNull(serializers.get(DEFAULT_SERIALIZER_PATH));
}
public static ControllerSerializer of(Path baseDirectory) {
var known = serializers.get(baseDirectory);
if(known != null) {
return known;
}
var result = new DefaultControllerSerializer(baseDirectory);
serializers.put(baseDirectory, result);
return result;
}
private DefaultControllerSerializer(Path baseDirectory) {
this.baseDirectory = baseDirectory;
this.attributeStoreSerializers = new ConcurrentHashMap<>();
}
@Override
public StoreKeysPair newStoreKeysPair(UUID uuid, Long phoneNumber, Collection alias, ClientType clientType) {
var parsedPhoneNumber = PhoneNumber.ofNullable(phoneNumber);
var store = new Store(
uuid,
parsedPhoneNumber.orElse(null),
this,
clientType,
alias,
null,
null,
false,
null,
Specification.Whatsapp.DEFAULT_NAME,
null,
null,
null,
null,
null,
null,
null,
null,
new LinkedHashMap<>(),
null,
null,
phoneNumber != null ? Jid.of(phoneNumber) : null,
null,
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
false,
false,
Clock.nowSeconds(),
ChatEphemeralTimer.OFF,
TextPreviewSetting.ENABLED_WITH_INFERENCE,
WebHistoryLength.standard(),
true,
true,
true,
UserAgent.ReleaseChannel.RELEASE,
clientType == ClientType.WEB ? CompanionDevice.web() : CompanionDevice.ios(false),
false
);
linkMetadata(store);
var registrationId = KeyHelper.registrationId();
var identityKeyPair = SignalKeyPair.random();
var keys = new Keys(
uuid,
parsedPhoneNumber.orElse(null),
this,
clientType,
alias,
registrationId,
SignalKeyPair.random(),
SignalKeyPair.random(),
identityKeyPair,
SignalKeyPair.random(),
SignalSignedKeyPair.of(registrationId, identityKeyPair),
null,
null,
new ArrayList<>(),
KeyHelper.phoneId(),
KeyHelper.deviceId(),
KeyHelper.identityId(),
null,
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
false,
false,
false
);
serializeKeys(keys, true);
return new StoreKeysPair(store, keys);
}
@Override
public Optional deserializeStoreKeysPair(UUID uuid, Long phoneNumber, String alias, ClientType clientType) {
if (uuid != null) {
var store = deserializeStore(clientType, uuid);
if(store.isEmpty()) {
return Optional.empty();
}
store.get().setSerializer(this);
attributeStore(store.get());
var keys = deserializeKeys(clientType, uuid);
if(keys.isEmpty()) {
return Optional.empty();
}
keys.get().setSerializer(this);
return Optional.of(new StoreKeysPair(store.get(), keys.get()));
}
if (phoneNumber != null) {
var store = deserializeStore(clientType, phoneNumber);
if(store.isEmpty()) {
return Optional.empty();
}
store.get().setSerializer(this);
attributeStore(store.get());
var keys = deserializeKeys(clientType, phoneNumber);
if(keys.isEmpty()) {
return Optional.empty();
}
keys.get().setSerializer(this);
return Optional.of(new StoreKeysPair(store.get(), keys.get()));
}
if (alias != null) {
var store = deserializeStore(clientType, alias);
if(store.isEmpty()) {
return Optional.empty();
}
store.get().setSerializer(this);
attributeStore(store.get());
var keys = deserializeKeys(clientType, alias);
if(keys.isEmpty()) {
return Optional.empty();
}
keys.get().setSerializer(this);
return Optional.of(new StoreKeysPair(store.get(), keys.get()));
}
return Optional.empty();
}
@Override
public LinkedList listIds(ClientType type) {
if (cachedUuids != null) {
return new ImmutableLinkedList<>(cachedUuids);
}
var directory = getHome(type);
if (Files.notExists(directory)) {
return ImmutableLinkedList.empty();
}
try (var walker = Files.walk(directory, 1).sorted(Comparator.comparing(this::getLastModifiedTime))) {
return cachedUuids = walker.map(this::parsePathAsId)
.flatMap(Optional::stream)
.collect(Collectors.toCollection(LinkedList::new));
} catch (IOException exception) {
return ImmutableLinkedList.empty();
}
}
@Override
public LinkedList listPhoneNumbers(ClientType type) {
if (cachedPhoneNumbers != null) {
return new ImmutableLinkedList<>(cachedPhoneNumbers);
}
var directory = getHome(type);
if (Files.notExists(directory)) {
return ImmutableLinkedList.empty();
}
try (var walker = Files.walk(directory, 1).sorted(Comparator.comparing(this::getLastModifiedTime))) {
return cachedPhoneNumbers = walker.map(this::parsePathAsPhoneNumber)
.flatMap(Optional::stream)
.collect(Collectors.toCollection(LinkedList::new));
} catch (IOException exception) {
return ImmutableLinkedList.empty();
}
}
private FileTime getLastModifiedTime(Path path) {
try {
return Files.getLastModifiedTime(path);
} catch (IOException exception) {
return FileTime.fromMillis(0);
}
}
private Optional parsePathAsId(Path file) {
try {
return Optional.of(UUID.fromString(file.getFileName().toString()));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}
private Optional parsePathAsPhoneNumber(Path file) {
try {
var longValue = Long.parseLong(file.getFileName().toString());
return PhoneNumber.ofNullable(longValue);
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}
@Override
public CompletableFuture serializeKeys(Keys keys, boolean async) {
if (cachedUuids != null && !cachedUuids.contains(keys.uuid())) {
cachedUuids.add(keys.uuid());
}
var outputFile = getSessionFile(keys.clientType(), keys.uuid().toString(), KEYS_NAME);
if (async) {
return CompletableFuture.runAsync(() -> writeFile(keys, KEYS_NAME, outputFile))
.exceptionallyAsync(this::onError);
}
writeFile(keys, KEYS_NAME, outputFile);
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture serializeStore(Store store, boolean async) {
if (cachedUuids != null && !cachedUuids.contains(store.uuid())) {
cachedUuids.add(store.uuid());
}
var phoneNumber = store.phoneNumber().orElse(null);
if (cachedPhoneNumbers != null && !cachedPhoneNumbers.contains(phoneNumber)) {
cachedPhoneNumbers.add(phoneNumber);
}
var task = attributeStoreSerializers.get(store.uuid());
if (task != null && !task.isDone()) {
return task;
}
var chatsFutures = serializeChatsAsync(store);
var newslettersFutures = serializeNewslettersAsync(store);
var dependableFutures = Stream.of(chatsFutures, newslettersFutures)
.flatMap(Arrays::stream)
.toArray(CompletableFuture[]::new);
var result = CompletableFuture.allOf(dependableFutures).thenRunAsync(() -> {
var storePath = getSessionFile(store, STORE_NAME);
writeFile(store, STORE_NAME, storePath);
});
if (async) {
return result;
}
result.join();
return CompletableFuture.completedFuture(null);
}
private CompletableFuture>[] serializeChatsAsync(Store store) {
return store.chats()
.stream()
.map(chat -> serializeChatAsync(store, chat))
.toArray(CompletableFuture[]::new);
}
private CompletableFuture serializeChatAsync(Store store, Chat chat) {
if (!chat.hasUpdate()) {
return CompletableFuture.completedFuture(null);
}
var fileName = CHAT_PREFIX + chat.jid() + ".smile";
var outputFile = getSessionFile(store, fileName);
return CompletableFuture.runAsync(() -> writeFile(chat, fileName, outputFile))
.exceptionallyAsync(this::onError);
}
private Void onError(Throwable error) {
var logger = System.getLogger("Serializer");
logger.log(System.Logger.Level.ERROR, error);
return null;
}
private CompletableFuture>[] serializeNewslettersAsync(Store store) {
return store.newsletters()
.stream()
.map(newsletter -> serializeNewsletterAsync(store, newsletter))
.toArray(CompletableFuture[]::new);
}
private CompletableFuture serializeNewsletterAsync(Store store, Newsletter newsletter) {
var fileName = NEWSLETTER_PREFIX + newsletter.jid() + ".smile";
var outputFile = getSessionFile(store, fileName);
return CompletableFuture.runAsync(() -> writeFile(newsletter, fileName, outputFile));
}
private void writeFile(Object object, String fileName, Path outputFile) {
try {
var tempFile = Files.createTempFile(fileName, ".tmp");
try (var tempFileOutputStream = new GZIPOutputStream(Files.newOutputStream(tempFile))) {
Smile.writeValueAsBytes(tempFileOutputStream, object);
Files.move(tempFile, outputFile, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException exception) {
throw new UncheckedIOException("Cannot write file", exception);
}
}
@Override
public Optional deserializeKeys(ClientType type, UUID id) {
return deserializeKeysFromId(type, id.toString());
}
@Override
public Optional deserializeKeys(ClientType type, String alias) {
var file = getSessionDirectory(type, alias);
if (Files.notExists(file)) {
return Optional.empty();
}
try {
return deserializeKeysFromId(type, Files.readString(file));
} catch (IOException exception) {
return Optional.empty();
}
}
@Override
public Optional deserializeKeys(ClientType type, long phoneNumber) {
var file = getSessionDirectory(type, String.valueOf(phoneNumber));
if (Files.notExists(file)) {
return Optional.empty();
}
try {
return deserializeKeysFromId(type, Files.readString(file));
} catch (IOException exception) {
return Optional.empty();
}
}
private Optional deserializeKeysFromId(ClientType type, String id) {
var path = getSessionFile(type, id, "keys.smile");
try (var input = new GZIPInputStream(Files.newInputStream(path))) {
return Optional.of(Smile.readValue(input, Keys.class));
} catch (IOException exception) {
return Optional.empty();
}
}
@Override
public Optional deserializeStore(ClientType type, UUID id) {
return deserializeStoreFromId(type, id.toString());
}
@Override
public Optional deserializeStore(ClientType type, String alias) {
var file = getSessionDirectory(type, alias);
if (Files.notExists(file)) {
return Optional.empty();
}
try {
return deserializeStoreFromId(type, Files.readString(file));
} catch (IOException exception) {
return Optional.empty();
}
}
@Override
public Optional deserializeStore(ClientType type, long phoneNumber) {
var file = getSessionDirectory(type, String.valueOf(phoneNumber));
if (Files.notExists(file)) {
return Optional.empty();
}
try {
return deserializeStoreFromId(type, Files.readString(file));
} catch (IOException exception) {
return Optional.empty();
}
}
private Optional deserializeStoreFromId(ClientType type, String id) {
var path = getSessionFile(type, id, "store.smile");
if (Files.notExists(path)) {
return Optional.empty();
}
try (var input = new GZIPInputStream(Files.newInputStream(path))) {
return Optional.of(Smile.readValue(input, Store.class));
} catch (IOException exception) {
return Optional.empty();
}
}
@Override
public CompletableFuture attributeStore(Store store) {
var oldTask = attributeStoreSerializers.get(store.uuid());
if (oldTask != null) {
return oldTask;
}
var directory = getSessionDirectory(store.clientType(), store.uuid().toString());
if (Files.notExists(directory)) {
return CompletableFuture.completedFuture(null);
}
try (var walker = Files.walk(directory)) {
var futures = walker.map(entry -> handleStoreFile(store, entry))
.filter(Objects::nonNull)
.toArray(CompletableFuture[]::new);
var result = CompletableFuture.allOf(futures)
.thenRun(() -> attributeStoreContextualMessages(store));
attributeStoreSerializers.put(store.uuid(), result);
return result;
} catch (IOException exception) {
return CompletableFuture.failedFuture(exception);
}
}
// Do this after we have all the chats, or it won't work for obvious reasons
private void attributeStoreContextualMessages(Store store) {
store.chats()
.stream()
.flatMap(chat -> chat.messages().stream())
.forEach(message -> attributeStoreContextualMessage(store, message));
}
private void attributeStoreContextualMessage(Store store, HistorySyncMessage message) {
message.messageInfo()
.message()
.contentWithContext()
.flatMap(ContextualMessage::contextInfo)
.ifPresent(contextInfo -> attributeStoreContextInfo(store, contextInfo));
}
private void attributeStoreContextInfo(Store store, ContextInfo contextInfo) {
contextInfo.quotedMessageChatJid()
.flatMap(store::findChatByJid)
.ifPresent(contextInfo::setQuotedMessageChat);
}
private CompletableFuture handleStoreFile(Store store, Path entry) {
return switch (FileType.of(entry)) {
case NEWSLETTER -> CompletableFuture.runAsync(() -> deserializeNewsletter(store, entry));
case CHAT -> CompletableFuture.runAsync(() -> deserializeChat(store, entry));
case UNKNOWN -> null;
};
}
private enum FileType {
UNKNOWN(null),
CHAT(CHAT_PREFIX),
NEWSLETTER(NEWSLETTER_PREFIX);
private final String prefix;
FileType(String prefix) {
this.prefix = prefix;
}
private static FileType of(Path path) {
return Arrays.stream(values())
.filter(entry -> entry.prefix() != null && path.getFileName().toString().startsWith(entry.prefix()))
.findFirst()
.orElse(UNKNOWN);
}
private String prefix() {
return prefix;
}
}
@Override
public void deleteSession(Controller> controller) {
try {
var folderPath = getSessionDirectory(controller.clientType(), controller.uuid().toString());
delete(folderPath);
var phoneNumber = controller.phoneNumber().orElse(null);
if (phoneNumber == null) {
return;
}
var linkedFolderPath = getSessionDirectory(controller.clientType(), phoneNumber.toString());
Files.deleteIfExists(linkedFolderPath);
} catch (IOException exception) {
throw new UncheckedIOException("Cannot delete session", exception);
}
}
private Path delete(Path path) throws IOException {
return Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
@Override
public void linkMetadata(Controller> controller) {
controller.phoneNumber()
.ifPresent(phoneNumber -> linkToUuid(controller.clientType(), controller.uuid(), phoneNumber.toString()));
controller.alias()
.forEach(alias -> linkToUuid(controller.clientType(), controller.uuid(), alias));
}
private void linkToUuid(ClientType type, UUID uuid, String string) {
try {
var link = getSessionDirectory(type, string);
Files.writeString(link, uuid.toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException ignored) {
}
}
private void deserializeChat(Store store, Path chatFile) {
try (var input = new GZIPInputStream(Files.newInputStream(chatFile))) {
var chat = Smile.readValue(input, Chat.class);
for (var message : chat.messages()) {
message.messageInfo().setChat(chat);
}
store.addChatDirect(chat);
} catch (IOException exception) {
store.addChatDirect(rescueChat(chatFile));
}
}
private Chat rescueChat(Path entry) {
try {
Files.deleteIfExists(entry);
} catch (IOException ignored) {
}
var chatName = entry.getFileName().toString()
.replaceFirst(CHAT_PREFIX, "")
.replace(".smile", "")
.replaceAll("~~", ":");
return new ChatBuilder()
.jid(Jid.of(chatName))
.build();
}
private void deserializeNewsletter(Store store, Path newsletterFile) {
try (var input = new GZIPInputStream(Files.newInputStream(newsletterFile))) {
var newsletter = Smile.readValue(input, Newsletter.class);
for (var message : newsletter.messages()) {
message.setNewsletter(newsletter);
}
store.addNewsletter(newsletter);
} catch (IOException exception) {
store.addNewsletter(rescueNewsletter(newsletterFile));
}
}
private Newsletter rescueNewsletter(Path entry) {
try {
Files.deleteIfExists(entry);
} catch (IOException ignored) {
}
var newsletterName = entry.getFileName().toString()
.replaceFirst(CHAT_PREFIX, "")
.replace(".smile", "")
.replaceAll("~~", ":");
return new Newsletter(Jid.of(newsletterName), null, null, null);
}
private Path getHome(ClientType type) {
return baseDirectory.resolve(type == ClientType.MOBILE ? "mobile" : "web");
}
private Path getSessionDirectory(ClientType clientType, String path) {
try {
var result = getHome(clientType).resolve(path);
Files.createDirectories(result.getParent());
return result;
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
private Path getSessionFile(Store store, String fileName) {
try {
var fixedName = fileName.replaceAll(":", "~~");
var result = getSessionFile(store.clientType(), store.uuid().toString(), fixedName);
Files.createDirectories(result.getParent());
return result;
} catch (IOException exception) {
throw new UncheckedIOException("Cannot create directory", exception);
}
}
private Path getSessionFile(ClientType clientType, String uuid, String fileName) {
try {
var result = getSessionDirectory(clientType, uuid).resolve(fileName);
Files.createDirectories(result.getParent());
return result;
} catch (IOException exception) {
throw new UncheckedIOException("Cannot create directory", exception);
}
}
private static class ImmutableLinkedList extends LinkedList {
@SuppressWarnings({"rawtypes", "unchecked"})
private static final ImmutableLinkedList EMPTY = new ImmutableLinkedList(new LinkedList());
private final LinkedList delegate;
@SuppressWarnings("unchecked")
private static ImmutableLinkedList empty() {
return EMPTY;
}
private ImmutableLinkedList(LinkedList delegate) {
this.delegate = delegate;
}
@Override
public E getFirst() {
return delegate.getFirst();
}
@Override
public E getLast() {
return delegate.getLast();
}
@Override
public boolean contains(Object o) {
return delegate.contains(o);
}
@Override
public int size() {
return delegate.size();
}
@Override
public void clear() {
delegate.clear();
}
@Override
public E get(int index) {
return delegate.get(index);
}
@Override
public int indexOf(Object o) {
return delegate.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return delegate.lastIndexOf(o);
}
@Override
public E peek() {
return delegate.peek();
}
@Override
public E element() {
return delegate.element();
}
@Override
public E poll() {
return delegate.poll();
}
@Override
public boolean offer(E e) {
return delegate.offer(e);
}
@Override
public boolean offerFirst(E e) {
return delegate.offerFirst(e);
}
@Override
public boolean offerLast(E e) {
return delegate.offerLast(e);
}
@Override
public E peekFirst() {
return delegate.peekFirst();
}
@Override
public E peekLast() {
return delegate.peekLast();
}
@Override
public E pollFirst() {
return delegate.pollFirst();
}
@Override
public E pollLast() {
return delegate.pollLast();
}
@Override
public void push(E e) {
delegate.push(e);
}
@Override
public E pop() {
return delegate.pop();
}
@Override
public ListIterator listIterator(int index) {
return delegate.listIterator(index);
}
@Override
public Iterator descendingIterator() {
return delegate.descendingIterator();
}
@SuppressWarnings("MethodDoesntCallSuperMethod")
@Override
public Object clone() {
return delegate.clone();
}
@Override
public Object[] toArray() {
return delegate.toArray();
}
@Override
public T[] toArray(T[] a) {
return delegate.toArray(a);
}
@Override
public Spliterator spliterator() {
return delegate.spliterator();
}
@Override
public LinkedList reversed() {
return delegate.reversed();
}
@Override
public void replaceAll(UnaryOperator operator) {
delegate.replaceAll(operator);
}
@Override
public void sort(Comparator super E> c) {
delegate.sort(c);
}
@Override
public T[] toArray(IntFunction generator) {
return delegate.toArray(generator);
}
@Override
public Stream stream() {
return delegate.stream();
}
@Override
public Stream parallelStream() {
return delegate.parallelStream();
}
@Override
public void forEach(Consumer super E> action) {
delegate.forEach(action);
}
@Override
public boolean add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
@Override
public void addLast(E e) {
throw new UnsupportedOperationException();
}
@Override
public void addFirst(E e) {
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(Collection extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(int index, Collection extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
@Override
public E remove() {
throw new UnsupportedOperationException();
}
@Override
public E removeFirst() {
throw new UnsupportedOperationException();
}
@Override
public E removeLast() {
throw new UnsupportedOperationException();
}
@Override
protected void removeRange(int fromIndex, int toIndex) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeFirstOccurrence(Object o) {
throw new UnsupportedOperationException();
}
@Override
public E remove(int index) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeIf(Predicate super E> filter) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeLastOccurrence(Object o) {
throw new UnsupportedOperationException();
}
@Override
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
}
}