DiagramSerialization.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
 * All Rights Reserved.
 *
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 *
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 *
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 *
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.ge.internal.diagram.runtime;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.osate.ge.CanonicalBusinessObjectReference;
import org.osate.ge.DiagramType;
import org.osate.ge.RelativeBusinessObjectReference;
import org.osate.ge.aadl2.internal.AadlReferenceUtil;
import org.osate.ge.aadl2.internal.diagramtypes.CustomDiagramType;
import org.osate.ge.aadl2.internal.diagramtypes.PackageDiagramType;
import org.osate.ge.aadl2.internal.diagramtypes.StructureDiagramType;
import org.osate.ge.diagram.Diagram;
import org.osate.ge.graphics.Color;
import org.osate.ge.graphics.Dimension;
import org.osate.ge.graphics.Point;
import org.osate.ge.graphics.Style;
import org.osate.ge.graphics.StyleBuilder;
import org.osate.ge.internal.GraphicalEditorException;
import org.osate.ge.internal.businessobjecthandlers.InternalReferenceUtil;
import org.osate.ge.internal.diagram.runtime.types.UnrecognizedDiagramType;
import org.osate.ge.internal.model.EmbeddedBusinessObject;
import org.osate.ge.internal.services.ExtensionRegistryService;
import org.osate.ge.internal.services.impl.DeclarativeReferenceType;

/**
 * Utility class with members related to reading and writing the diagram file format used by the editor.
 *
 */
public final class DiagramSerialization {
	/**
	 * Private constructor to prevent instantiation.
	 */
	private DiagramSerialization() {
	}

	/**
	 * The version of the diagram file format. This number should be incremented when changes are made to the format. This value
	 * is stored in the diagram file and is used to detect potential compatibility issues.
	 */
	public final static int FORMAT_VERSION = 7;

	private static Comparator<DiagramElement> elementComparator = (e1, e2) -> e1.getRelativeReference()
			.compareTo(e2.getRelativeReference());

	/**
	 * Loads the serialized diagram
	 * @param uri the URI specifying the location of the diagram file
	 * @return the serialized diagram
	 */
	public static org.osate.ge.diagram.Diagram readMetaModelDiagram(final URI uri) {
		Objects.requireNonNull(uri, "uri must not be null");

		// Load the resource
		final ResourceSet rs = new ResourceSetImpl();
		final Resource resource = rs.createResource(uri);
		try {
			resource.load(Collections.singletonMap(XMLResource.OPTION_RECORD_UNKNOWN_FEATURE, true));
			if (resource.getContents().size() == 0
					|| !(resource.getContents().get(0) instanceof org.osate.ge.diagram.Diagram)) {
				throw new GraphicalEditorException("Unable to load diagram.");
			}

			final org.osate.ge.diagram.Diagram mmDiagram = (org.osate.ge.diagram.Diagram) resource.getContents().get(0);
			return mmDiagram;
		} catch (final IOException e) {
			throw new GraphicalEditorException(e);
		}
	}

	/**
	 * Converts the serialized diagram to a runtime diagram
	 * @param project the project in which the serialized diagram is contained.
	 * @param mmDiagram the serialized diagram
	 * @param extRegistry the Eclipse extension registry
	 * @return the runtime diagram
	 */
	public static AgeDiagram createAgeDiagram(final IProject project, final org.osate.ge.diagram.Diagram mmDiagram,
			final ExtensionRegistryService extRegistry) {
		Objects.requireNonNull(extRegistry, "extRegistry is null");

		// Set the id which should be used for new diagram elements.
		final AgeDiagram ageDiagram = new AgeDiagram();

		// Read the diagram configuration

		// Set the diagram type
		final String diagramTypeId;
		if (mmDiagram.getConfig() == null || mmDiagram.getConfig().getType() == null) {
			// Assign a diagram type ID if the diagram does not have specify one.
			String autoAssignedDiagramTypeId = CustomDiagramType.ID;

			// Set the diagram type based on the diagram's context
			if (mmDiagram.getConfig() != null) {
				final CanonicalBusinessObjectReference contextRef = convert(mmDiagram.getConfig().getContext());
				if (contextRef != null && contextRef.getSegments().size() > 1) {
					if (DeclarativeReferenceType.PACKAGE.getId().equals(contextRef.getSegments().get(0))) {
						autoAssignedDiagramTypeId = PackageDiagramType.ID;
					} else if (DeclarativeReferenceType.CLASSIFIER.getId().equals(contextRef.getSegments().get(0))) {
						autoAssignedDiagramTypeId = StructureDiagramType.ID;
					}
				}
			}

			diagramTypeId = autoAssignedDiagramTypeId;
		} else {
			diagramTypeId = mmDiagram.getConfig().getType();
		}
		final DiagramType diagramType = extRegistry.getDiagramTypeById(diagramTypeId)
				.orElseGet(() -> new UnrecognizedDiagramType(diagramTypeId));

		final DiagramConfigurationBuilder configBuilder = new DiagramConfigurationBuilder(diagramType, false);

		if (mmDiagram.getConfig() != null) {
			final org.osate.ge.diagram.DiagramConfiguration mmDiagramConfig = mmDiagram.getConfig();
			configBuilder.contextBoReference(convert(mmDiagramConfig.getContext()));

			final org.osate.ge.diagram.AadlPropertiesSet enabledAadlProperties = mmDiagramConfig
					.getEnabledAadlProperties();
			if (enabledAadlProperties != null) {
				for (final String enabledProperty : enabledAadlProperties.getProperty()) {
					configBuilder.addAadlProperty(enabledProperty);
				}
			}

			configBuilder.connectionPrimaryLabelsVisible(mmDiagramConfig.getConnectionPrimaryLabelsVisible());
		}

		// Ensure UUIDs are valid and handle migration from legacy id's.
		final Map<Long, UUID> legacyIdToUuidMap = new HashMap<>();
		ensureIdsAreValid(mmDiagram, legacyIdToUuidMap);
		updateReferencesToLegacyIds(mmDiagram, legacyIdToUuidMap);

		ageDiagram.modify("Configure Diagram", m -> {
			m.setDiagramConfiguration(configBuilder.build());
		});

		// Read elements
		ageDiagram.modify("Read from File", m -> {
			readElements(project, m, ageDiagram, mmDiagram, legacyIdToUuidMap);
		});

		return ageDiagram;
	}

	/**
	 * Ensures all elements have a UUID. Migrates legacy id's to UUIDs.
	 * @param diagram
	 * @param legacyIdToUuidMap map to populate with a mapping form legacy id's to the UUIDs. Needed to migrate references.
	 */
	private static void ensureIdsAreValid(final org.osate.ge.diagram.Diagram diagram,
			Map<Long, UUID> legacyIdToUuidMap) {
		final Iterator<Object> it = EcoreUtil.getAllProperContents(diagram, true);
		while (it.hasNext()) {
			final Object o = it.next();
			if (o instanceof org.osate.ge.diagram.DiagramElement) {
				final org.osate.ge.diagram.DiagramElement e = (org.osate.ge.diagram.DiagramElement) o;
				if (e.getUuid() == null) {
					final UUID newUuid = UUID.randomUUID();
					e.setUuid(newUuid.toString());

					if (e.getId() != null) {
						legacyIdToUuidMap.put(e.getId(), newUuid);
					}
				}
			}
		}
	}

	private static void updateReferencesToLegacyIds(final org.osate.ge.diagram.Diagram diagram,
			Map<Long, UUID> legacyIdToUuidMap) {
		if (legacyIdToUuidMap.size() == 0) {
			return;
		}

		// Older versions of property value groups use legacy ids.
		final Iterator<Object> it = EcoreUtil.getAllProperContents(diagram, true);
		while (it.hasNext()) {
			final Object o = it.next();
			if (o instanceof org.osate.ge.diagram.RelativeBusinessObjectReference) {
				org.osate.ge.diagram.RelativeBusinessObjectReference ref = (org.osate.ge.diagram.RelativeBusinessObjectReference) o;
				if (ref.getSeg().size() == 3
						&& Objects.equals(ref.getSeg().get(0), AadlReferenceUtil.PROPERTY_VALUE_GROUP_KEY)) {
					final int idSegmentIndex = 2;
					final UUID referencedUuid = legacyIdToUuidMap.get(Long.parseLong(ref.getSeg().get(idSegmentIndex)));

					if (referencedUuid != null) {
						ref.getSeg().set(idSegmentIndex, referencedUuid.toString());
					}
				}
			}
		}
	}

	/**
	 * Converts a serialized relative business object reference to a runtime relative business object reference
	 * @param ref the reference to convert
	 * @return the runtime relative business object reference
	 */
	public static RelativeBusinessObjectReference convert(
			final org.osate.ge.diagram.RelativeBusinessObjectReference ref) {
		final String[] segs = toReferenceSegments(ref);
		return segs == null ? null : new RelativeBusinessObjectReference(segs);
	}

	/**
	 * Converts a serialized canonical business object reference to a runtime canonical business object reference
	 * @param ref the reference to convert
	 * @return the runtime canonical business object reference
	 */
	public static CanonicalBusinessObjectReference convert(
			final org.osate.ge.diagram.CanonicalBusinessObjectReference ref) {
		final String[] segs = toReferenceSegments(ref);
		return segs == null ? null : new CanonicalBusinessObjectReference(segs);
	}

	private static String[] toReferenceSegments(final org.osate.ge.diagram.Reference ref) {
		return ref == null || ref.getSeg().size() == 0 ? null : ref.getSeg().toArray(new String[ref.getSeg().size()]);
	}

	private static void readElements(final IProject project, final DiagramModification m, final DiagramNode container,
			final org.osate.ge.diagram.DiagramNode mmContainer, final Map<Long, UUID> legacyIdToUuidMap) {
		for (final org.osate.ge.diagram.DiagramElement mmElement : mmContainer.getElement()) {
			createElement(project, m, container, mmElement, legacyIdToUuidMap);
		}
	}

	private static void createElement(final IProject project, final DiagramModification m, final DiagramNode container,
			final org.osate.ge.diagram.DiagramElement mmChild, final Map<Long, UUID> legacyIdToUuidMap) {
		final String[] refSegs = toReferenceSegments(mmChild.getBo());
		if (refSegs == null) {
			throw new GraphicalEditorException("Invalid element. Invalid business object reference.");
		}

		final RelativeBusinessObjectReference relReference = new RelativeBusinessObjectReference(refSegs);
		final Object bo = InternalReferenceUtil.createEmbeddedObject(relReference, mmChild.getBoData());

		// Set the ID
		final UUID uuid = UUID.fromString(mmChild.getUuid());
		final DiagramElement newElement = new DiagramElement(container, bo, null, relReference, uuid);

		// Size and Position
		newElement.setPosition(convertPoint(mmChild.getPosition()));
		newElement.setSize(convertDimension(mmChild.getSize()));

		// Dock Area
		final String dockAreaId = mmChild.getDockArea();
		if (dockAreaId != null) {
			final DockArea dockArea = DockArea.getById(dockAreaId);
			if (dockArea != null) {
				newElement.setDockArea(dockArea);
			}
		}

		// Style
		final Boolean showAsImage = mmChild.getShowAsImage();
		final Color background = mmChild.getBackground() != null ? parseColor(mmChild.getBackground()) : null;
		final IPath image = mmChild.getImage() != null
				? project.getFile(Path.fromPortableString(mmChild.getImage())).getFullPath()
						: null;
		final Color fontColor = mmChild.getFontColor() != null ? parseColor(mmChild.getFontColor()) : null;
		final Color outline = mmChild.getOutline() != null ? parseColor(mmChild.getOutline()) : null;
		final Double lineWidth = mmChild.getLineWidth();
		final Double fontSize = mmChild.getFontSize();
		final Boolean primaryLabelVisible = mmChild.getPrimaryLabelVisible();

		newElement.setStyle(StyleBuilder.create()
				.backgroundColor(background)
				.showAsImage(showAsImage)
				.imagePath(image)
				.fontColor(fontColor)
				.outlineColor(outline)
				.fontSize(fontSize)
				.lineWidth(lineWidth)
				.primaryLabelVisible(primaryLabelVisible)
				.build());

		// Bendpoints
		final org.osate.ge.diagram.BendpointList mmBendpoints = mmChild.getBendpoints();
		if (mmBendpoints == null) {
			newElement.setBendpoints(Collections.emptyList());
		} else {
			newElement.setBendpoints(mmBendpoints.getPoint()
					.stream()
					.map(DiagramSerialization::convertPoint)
					.collect(Collectors.toList()));
		}

		// Primary Label Position (Only Supported for Connections)
		newElement.setConnectionPrimaryLabelPosition(convertPoint(mmChild.getPrimaryLabelPosition()));

		// Add the element
		m.addElement(newElement);

		// Create children
		readElements(project, m, newElement, mmChild, legacyIdToUuidMap);
	}

	private static Point convertPoint(final org.osate.ge.diagram.Point mmPoint) {
		if (mmPoint == null) {
			return null;
		}

		return new Point((int) Math.round(mmPoint.getX()), (int) Math.round(mmPoint.getY()));
	}

	private static Dimension convertDimension(final org.osate.ge.diagram.Dimension mmDimension) {
		if (mmDimension == null) {
			return null;
		}

		return new Dimension((int) Math.round(mmDimension.getWidth()), (int) Math.round(mmDimension.getHeight()));
	}

	/**
	 * Serialized the specified runtime diagram and writes is to a file.
	 * @param project the project in which the diagram is contained.
	 * @param diagram the runtime diagram to serialize
	 * @param uri the URI specifying the file to which to write the serialized diagram
	 */
	public static void write(final IProject project, final AgeDiagram diagram, final URI uri) {
		// Convert from the runtime format to the metamodel format which is stored
		final org.osate.ge.diagram.Diagram mmDiagram = new Diagram();
		mmDiagram.setFormatVersion(FORMAT_VERSION);
		final org.osate.ge.diagram.DiagramConfiguration mmConfig = new org.osate.ge.diagram.DiagramConfiguration();
		mmDiagram.setConfig(mmConfig);

		// Populate the diagram configuration
		final DiagramConfiguration config = diagram.getConfiguration();
		mmConfig.setType(config.getDiagramType().getId());
		mmConfig.setContext(
				config.getContextBoReference() == null ? null : config.getContextBoReference().toMetamodel());

		mmConfig.setConnectionPrimaryLabelsVisible(config.getConnectionPrimaryLabelsVisible());

		final org.osate.ge.diagram.AadlPropertiesSet enabledProperties = new org.osate.ge.diagram.AadlPropertiesSet();
		mmConfig.setEnabledAadlProperties(enabledProperties);
		for (final String enabledPropertyName : config.getEnabledAadlPropertyNames()) {
			enabledProperties.getProperty().add(enabledPropertyName);
		}

		convertElementsToMetamodel(project, mmDiagram, diagram.getChildren());

		// Save the resource
		final ResourceSet rs = new ResourceSetImpl();
		final Resource resource = rs.createResource(uri);
		resource.getContents().add(mmDiagram);
		try {
			resource.save(Collections.emptyMap());
		} catch (IOException e) {
			throw new GraphicalEditorException(e);
		}
	}

	/**
	 * Serialized the specified runtime diagram elements and adds them to a serialized diagram element. Recursive
	 * @param project the project in which the diagram elements are contained
	 * @param mmContainer the container of the new diagram elements
	 * @param elements the elements to serialized
	 */
	private static void convertElementsToMetamodel(final IProject project,
			final org.osate.ge.diagram.DiagramNode mmContainer, Collection<DiagramElement> elements) {
		// Sort elements to ensure a consistent ordering after serialization
		elements = elements.stream().sorted(elementComparator).collect(Collectors.toList());

		if (elements.size() > 0) {
			for (final DiagramElement e : elements) {
				convertElementToMetamodel(project, mmContainer, e);
			}
		}
	}

	private static void convertElementToMetamodel(final IProject project,
			final org.osate.ge.diagram.DiagramNode mmContainer, final DiagramElement e) {
		// Write BO Reference
		final org.osate.ge.diagram.DiagramElement newElement = new org.osate.ge.diagram.DiagramElement();
		mmContainer.getElement().add(newElement);

		newElement.setUuid(e.getId().toString());
		newElement.setBo(e.getRelativeReference().toMetamodel());

		// Store embedded business object data.
		if (e.getBusinessObject() instanceof EmbeddedBusinessObject) {
			final EmbeddedBusinessObject bo = (EmbeddedBusinessObject) e.getBusinessObject();
			newElement.setBoData(bo.getData());
		}

		newElement.setManual(true);

		if (e.hasPosition()) {
			newElement.setPosition(e.getPosition().toMetamodel());
		}
		if (e.hasSize() && DiagramElementPredicates.isResizeable(e)) {
			newElement.setSize(e.getSize().toMetamodel());
		}

		if (e.getDockArea() != null && e.getDockArea() != DockArea.GROUP) { // Don't serialize null or group dock areas
			newElement.setDockArea(e.getDockArea().id);
		}

		final Style currentStyle = e.getStyle();
		final org.osate.ge.graphics.Color backgroundColor = currentStyle.getBackgroundColor();
		if (backgroundColor != null) {
			newElement.setBackground(colorToHex(backgroundColor));
		}

		final IPath image = currentStyle.getImagePath();
		if (image != null) {
			// Get image path relative to the diagram's project
			final String portablePath = image.makeRelativeTo(project.getFullPath()).toPortableString();
			newElement.setImage(portablePath);
			newElement.setShowAsImage(currentStyle.getShowAsImage());
		}

		final org.osate.ge.graphics.Color fontColor = currentStyle.getFontColor();
		if (fontColor != null) {
			newElement.setFontColor(colorToHex(fontColor));
		}

		final org.osate.ge.graphics.Color outlineColor = currentStyle.getOutlineColor();
		if (outlineColor != null) {
			newElement.setOutline(colorToHex(outlineColor));
		}

		final Double fontSize = currentStyle.getFontSize();
		if (fontSize != null) {
			newElement.setFontSize(fontSize);
		}

		final Double lineWidth = currentStyle.getLineWidth();
		if (lineWidth != null) {
			newElement.setLineWidth(lineWidth);
		}

		if (currentStyle.getPrimaryLabelVisible() != null) {
			newElement.setPrimaryLabelVisible(currentStyle.getPrimaryLabelVisible());
		}

		// Connection Specific
		if (e.getBendpoints().size() > 0) {
			final org.osate.ge.diagram.BendpointList mmBendpoints = new org.osate.ge.diagram.BendpointList();
			newElement.setBendpoints(mmBendpoints);

			for (final Point bendpoint : e.getBendpoints()) {
				mmBendpoints.getPoint().add(bendpoint.toMetamodel());
			}
		}

		if (e.getConnectionPrimaryLabelPosition() != null) {
			newElement.setPrimaryLabelPosition(e.getConnectionPrimaryLabelPosition().toMetamodel());
		}

		convertElementsToMetamodel(project, newElement, e.getChildren());
	}

	// Create hex string from color
	private static String colorToHex(final org.osate.ge.graphics.Color color) {
		return "#" + String.format("%02x", color.getRed()) + String.format("%02x", color.getGreen())
		+ String.format("%02x", color.getBlue());
	}

	// Create color from hex string
	private static Color parseColor(final String color) {
		return new Color(Integer.parseInt(color.substring(1, 3), 16), Integer.parseInt(color.substring(3, 5), 16),
				Integer.parseInt(color.substring(5), 16));
	}
}