eo-runtime/src/main/java/org/eolang/PhDefault.java

Summary

Maintainability
C
1 day
Test Coverage
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2024 Objectionary.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.eolang;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * A simple object.
 *
 * The class is thread-safe.
 * @since 0.1
 * @checkstyle DesignForExtensionCheck (500 lines)
 */
@Versionized
@SuppressWarnings({"PMD.TooManyMethods", "PMD.GodClass"})
public class PhDefault implements Phi, Cloneable {
    /**
     * Vertices.
     */
    private static final Vertices VTX = new Vertices();

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(PhDefault.class.getName());

    /**
     * Attribute name matcher.
     */
    private static final Pattern SORTABLE = Pattern.compile("^[a-z].*$");

    /**
     * Attributes nesting level.
     *
     * @todo #2251:90min It is necessary to call {@link ThreadLocal#remove()} on
     *  {@link PhDefault#NESTING} to prevent memory leaks. We should either find a place where this
     *  variable can be removed, or, if this is not possible
     *  (see https://github.com/objectionary/eo/pull/1930), come up with another solution.
     */
    @SuppressWarnings("java:S5164")
    private static final ThreadLocal<Integer> NESTING = ThreadLocal.withInitial(() -> 0);

    /**
     * Identity of it (the ID of the vertex).
     * @checkstyle VisibilityModifierCheck (2 lines)
     */
    private int vertex;

    /**
     * Data.
     * @checkstyle VisibilityModifierCheck (2 lines)
     */
    private AtomicReference<byte[]> data;

    /**
     * Forma of it.
     */
    private final String form;

    /**
     * Order of their names.
     */
    private final Map<Integer, String> order;

    /**
     * Attributes.
     */
    private Map<String, Attr> attrs;

    /**
     * Ctor.
     */
    @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
    public PhDefault() {
        this.data = new AtomicReference<>(null);
        this.vertex = PhDefault.VTX.next();
        this.form = this.getClass().getName();
        this.attrs = new HashMap<>(0);
        this.order = new HashMap<>(0);
        this.add(Attr.RHO, new AtRho());
    }

    @Override
    public boolean equals(final Object obj) {
        return obj instanceof Phi && this.hashCode() == obj.hashCode();
    }

    @Override
    public int hashCode() {
        return this.vertex;
    }

    @Override
    public String φTerm() {
        final List<String> list = new ArrayList<>(this.attrs.size());
        final String format = "%s ↦ %s";
        if (this.data.get() != null) {
            list.add(
                String.format(
                    format, Attr.DELTA, new BytesOf(this.data.get()).asString()
                )
            );
        }
        for (final Map.Entry<String, Attr> ent : this.attrs.entrySet().stream().filter(
            e -> !e.getKey().equals(Attr.RHO)
        ).collect(Collectors.toList())) {
            final String attr = String.format(
                format,
                ent.getKey(),
                ent.getValue().φTerm()
            );
            list.add(attr);
        }
        if (this instanceof Atom) {
            list.add(String.format(format, Attr.LAMBDA, "Lambda"));
        }
        Collections.sort(list);
        String txt = this.oname();
        if (!list.isEmpty()) {
            txt = String.format(
                "ν%d·%s⟦\n\t%s\n⟧", this.vertex, txt,
                new Indented(String.join(",\n", list))
            );
        }
        return txt;
    }

    @Override
    public String toString() {
        String result = String.format(
            "%sν%d",
            this.getClass().getCanonicalName(),
            this.vertex
        );
        if (this.data.get() != null) {
            result = String.format(
                "%s=%s",
                result,
                new BytesOf(this.data.get()).asString()
            );
        }
        return result;
    }

    @Override
    public final Phi copy() {
        try {
            final PhDefault copy = (PhDefault) this.clone();
            copy.vertex = PhDefault.VTX.next();
            copy.data = new AtomicReference<>(this.data.get());
            final Map<String, Attr> map = new HashMap<>(this.attrs.size());
            for (final Map.Entry<String, Attr> ent : this.attrs.entrySet()) {
                map.put(ent.getKey(), ent.getValue().copy(copy));
            }
            copy.attrs = map;
            return copy;
        } catch (final CloneNotSupportedException ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Override
    public boolean put(final int pos, final Phi object) {
        return this.put(this.attr(pos), object);
    }

    @Override
    public boolean put(final String name, final Phi object) {
        if (!this.attrs.containsKey(name)) {
            throw new ExUnset(
                String.format(
                    "Can't #put(%s, %s) to %s, because %s is absent",
                    name, object, this, name
                )
            );
        }
        return new AtSafe(this.named(this.attrs.get(name), name)).put(object);
    }

    @Override
    public Phi take(final String name) {
        PhDefault.NESTING.set(PhDefault.NESTING.get() + 1);
        final Phi object;
        if (this.attrs.containsKey(name)) {
            object = new AtSafe(
                this.named(
                    new AtSetRho(
                        this.attrs.get(name),
                        this,
                        name
                    ),
                    name
                )
            ).get();
        } else if (name.equals(Attr.LAMBDA)) {
            object = new AtSafe(
                this.named(
                    new AtSetRho(
                        new AtFormed(new AtomSafe((Atom) this)::lambda),
                        this,
                        name
                    ),
                    name
                )
            ).get();
        } else if (this instanceof Atom) {
            object = this.take(Attr.LAMBDA).take(name);
        } else if (this.attrs.containsKey(Attr.PHI)) {
            object = this.take(Attr.PHI).take(name);
        } else {
            object = new AtSafe(
                this.named(
                    new AtAbsent(
                        name,
                        String.format(
                            "Can't #take(\"%s\"), the attribute is absent among other %d attrs of %s:(%s), %s and %s are also absent",
                            name,
                            this.attrs.size(),
                            this.form,
                            String.join(", ", this.attrs.keySet()),
                            Attr.PHI,
                            Attr.LAMBDA
                        )
                    ),
                    name
                )
            ).get();
        }
        PhDefault.debug(
            String.format(
                "%s\uD835\uDD38('%s' for %s) ➜ %s",
                PhDefault.padding(),
                name,
                this,
                object
            )
        );
        PhDefault.NESTING.set(PhDefault.NESTING.get() - 1);
        return object;
    }

    @Override
    public void attach(final byte[] bytes) {
        synchronized (this.data) {
            if (this.data.get() != null) {
                throw new ExFailure(
                    "Data is already attached to the object, can't reattach"
                );
            }
            this.data.set(bytes);
        }
    }

    @Override
    public byte[] delta() {
        final byte[] bytes;
        if (this.data.get() != null) {
            bytes = this.data.get();
        } else if (this instanceof Atom) {
            bytes = this.take(Attr.LAMBDA).delta();
        } else if (this.attrs.containsKey(Attr.PHI)) {
            bytes = this.take(Attr.PHI).delta();
        } else {
            throw new ExFailure(
                "There's no data in the object, can't take it"
            );
        }
        return bytes;
    }

    @Override
    public String locator() {
        return "?";
    }

    @Override
    public String forma() {
        return this.form;
    }

    /**
     * Add new attribute.
     *
     * This method can only be called from child classes, in their
     * constructors, when they declare their attributes. This is why it's
     * protected. Not the brightest design, I admit.
     * @param name The name
     * @param attr The attr
     */
    protected final void add(final String name, final Attr attr) {
        if (PhDefault.SORTABLE.matcher(name).matches()) {
            this.order.put(this.order.size(), name);
        }
        this.attrs.put(name, attr);
    }

    /**
     * Get attribute name by position.
     * @param pos Position of the attribute
     * @return Attribute name
     */
    private String attr(final int pos) {
        if (0 > pos) {
            throw new ExFailure(
                String.format(
                    "Attribute position can't be negative (%d)",
                    pos
                )
            );
        }
        if (this.order.isEmpty()) {
            throw new ExFailure(
                String.format(
                    "There are no attributes here, can't read the %d-th one",
                    pos
                )
            );
        }
        if (!this.order.containsKey(pos)) {
            throw new ExFailure(
                String.format(
                    "%s has just %d attribute(s), can't read the %d-th one",
                    this,
                    this.order.size(),
                    pos
                )
            );
        }
        return this.order.get(pos);
    }

    /**
     * Make named attribute.
     * @param attr The original attr
     * @param name The name of it
     * @return Named one
     */
    private Attr named(final Attr attr, final String name) {
        return new AtNamed(
            String.format(
                "%s#%s",
                this.getClass().getCanonicalName(), name
            ),
            String.format(
                "%s.%s",
                this.oname(), name
            ),
            this,
            attr
        );
    }

    /**
     * Get its object name, as in source code.
     * @return The name
     */
    private String oname() {
        String txt = this.getClass().getSimpleName();
        final XmirObject xmir = this.getClass().getAnnotation(XmirObject.class);
        if (null != xmir) {
            txt = xmir.oname();
            if ("@".equals(txt)) {
                txt = "φ";
            }
        }
        return txt;
    }

    /**
     * Log debug message for PhDefault.
     * @param msg Message to log
     */
    private static void debug(final String msg) {
        if (PhDefault.LOGGER.isLoggable(Level.FINE)) {
            PhDefault.LOGGER.log(
                Level.FINE,
                msg
            );
        }
    }

    /**
     * Padding according to current {@link #NESTING} level.
     * @return Padding string.
     */
    private static String padding() {
        return String.join("", Collections.nCopies(PhDefault.NESTING.get(), "·"));
    }
}