All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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 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 c) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean addAll(int index, Collection 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 filter) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean removeLastOccurrence(Object o) {
            throw new UnsupportedOperationException();
        }

        @Override
        public E set(int index, E element) {
            throw new UnsupportedOperationException();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy