NetworkIndex.java

/**
 * Copyright (c) 2016, All partners of the iTesla project (http://www.itesla-project.eu/consortium)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.iidm.network.impl;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.powsybl.commons.PowsyblException;
import com.powsybl.iidm.network.Identifiable;

import java.io.PrintStream;
import java.util.*;

/**
 *
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
class NetworkIndex {

    private final Map<String, Identifiable<?>> objectsById = new HashMap<>();
    private final Map<String, String> idByAlias = new HashMap<>();

    private final Map<Class<? extends Identifiable>, Set<Identifiable<?>>> objectsByClass = new HashMap<>();

    static void checkId(String id) {
        if (id == null || id.isEmpty()) {
            throw new PowsyblException("Invalid id '" + id + "'");
        }
    }

    static String getUniqueId() {
        return UUID.randomUUID().toString();
    }

    void checkAndAdd(Identifiable<?> obj) {
        checkId(obj.getId());
        if (objectsById.containsKey(obj.getId())) {
            throw new PowsyblException("Object (" + obj.getClass().getName()
                    + ") '" + obj.getId() + "' already exists");
        }
        objectsById.put(obj.getId(), obj);
        obj.getAliases().forEach(alias -> addAlias(obj, alias));

        Set<Identifiable<?>> all = objectsByClass.computeIfAbsent(obj.getClass(), k -> new LinkedHashSet<>());
        all.add(obj);
    }

    boolean addAlias(Identifiable<?> obj, String alias) {
        Identifiable<?> aliasConflict = objectsById.get(alias);
        if (aliasConflict != null) {
            if (aliasConflict.equals(obj)) {
                // Silently ignore affecting the objects id to its own aliases
                return false;
            }
            String message = String.format("Object (%s) with alias '%s' cannot be created because alias already refers to object (%s) with ID '%s'",
                    obj.getClass(),
                    alias,
                    aliasConflict.getClass(),
                    aliasConflict.getId());
            throw new PowsyblException(message);
        }
        String idForAlias = idByAlias.get(alias);
        if (idForAlias != null) {
            aliasConflict = objectsById.get(idForAlias);
            if (aliasConflict.equals(obj)) {
                // Silently ignore affecting the same alias twice to an object
                return false;
            }
            String message = String.format("Object (%s) with alias '%s' cannot be created because alias already refers to object (%s) with ID '%s'",
                    obj.getClass(),
                    alias,
                    aliasConflict.getClass(),
                    aliasConflict.getId());
            throw new PowsyblException(message);
        }
        idByAlias.put(alias, obj.getId());
        return true;
    }

    public <I extends Identifiable<I>> void removeAlias(Identifiable<?> obj, String alias) {
        String idForAlias = idByAlias.get(alias);
        if (idForAlias == null) {
            throw new PowsyblException(String.format("No alias '%s' found in the network", alias));
        } else if (!idForAlias.equals(obj.getId())) {
            throw new PowsyblException(String.format("Alias '%s' do not correspond to object '%s'", alias, obj.getId()));
        } else {
            idByAlias.remove(alias);
        }
    }

    Identifiable get(String idOrAlias) {
        String id = idByAlias.getOrDefault(idOrAlias, idOrAlias);
        checkId(id);
        return objectsById.get(id);
    }

    <T extends Identifiable> T get(String id, Class<T> clazz) {
        Identifiable<?> obj = get(id);
        if (obj != null && clazz.isAssignableFrom(obj.getClass())) {
            return (T) obj;
        } else {
            return null;
        }
    }

    Collection<Identifiable<?>> getAll() {
        return objectsById.values();
    }

    <T extends Identifiable> Set<T> getAll(Class<T> clazz) {
        Set<Identifiable<?>> all = objectsByClass.get(clazz);
        if (all == null) {
            return Collections.emptySet();
        }
        return (Set<T>) all;
    }

    boolean contains(String id) {
        String idFromPotentialAlias = idByAlias.getOrDefault(id, id);
        checkId(idFromPotentialAlias);
        return objectsById.containsKey(idFromPotentialAlias);
    }

    void remove(Identifiable obj) {
        checkId(obj.getId());
        Identifiable old = objectsById.remove(obj.getId());
        if (old == null || old != obj) {
            throw new PowsyblException("Object (" + obj.getClass().getName()
                    + ") '" + obj.getId() + "' not found");
        }
        obj.getAliases().forEach(idByAlias::remove);
        Set<Identifiable<?>> all = objectsByClass.get(obj.getClass());
        if (all != null) {
            all.remove(obj);
        }
    }

    void clean() {
        objectsById.clear();
        objectsByClass.clear();
    }

    /**
     * Compute intersection between this index and another one.
     * @param other the other index
     * @return list of objects id or alias that exist in both indexes organized by class.
     */
    Multimap<Class<? extends Identifiable>, String> intersection(NetworkIndex other) {
        Multimap<Class<? extends Identifiable>, String> intersection = HashMultimap.create();
        for (Map.Entry<Class<? extends Identifiable>, Set<Identifiable<?>>> entry : other.objectsByClass.entrySet()) {
            Class<? extends Identifiable> clazz = entry.getKey();
            Set<Identifiable<?>> objects = entry.getValue();
            for (Identifiable obj : objects) {
                if (objectsById.containsKey(obj.getId()) || idByAlias.containsKey(obj.getId())) {
                    intersection.put(clazz, obj.getId());
                }
                Set<String> aliases = obj.getAliases();
                for (String alias : aliases) {
                    if (objectsById.containsKey(alias) || idByAlias.containsKey(alias)) {
                        intersection.put(clazz, alias);
                    }
                }
            }
        }
        return intersection;
    }

    /**
     * Merge an other index into this one. At the end of the call the
     * other index is empty.
     * @param other the index to merge
     */
    void merge(NetworkIndex other) {
        for (Identifiable obj : other.objectsById.values()) {
            checkAndAdd(obj);
        }
        other.clean();
    }

    void printForDebug(PrintStream out) {
        for (Map.Entry<String, Identifiable<?>> entry : objectsById.entrySet()) {
            out.println(entry.getKey() + " " + System.identityHashCode(entry.getValue()));
        }
        for (Map.Entry<Class<? extends Identifiable>, Set<Identifiable<?>>> entry : objectsByClass.entrySet()) {
            out.println(entry.getKey() + " " + entry.getValue().stream().map(System::identityHashCode).toList());
        }
    }
}