Skip to content

Commit

Permalink
Move naming normalization to ModelNexus (eclipse#323)
Browse files Browse the repository at this point in the history
* fixed References in provider ecore

Signed-off-by: Juergen Albert <j.albert@data-in-motion.biz>

* Fixed remaining references in the models

Signed-off-by: Juergen Albert <j.albert@data-in-motion.biz>

* Models can now named however you like

Signed-off-by: Juergen Albert <j.albert@data-in-motion.biz>

---------

Signed-off-by: Juergen Albert <j.albert@data-in-motion.biz>
  • Loading branch information
juergen-albert committed Jan 26, 2024
1 parent 9ee83a5 commit df2810d
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.eclipse.sensinact.core.command.impl.ResourcePushHandler;
import org.eclipse.sensinact.core.model.ResourceType;
import org.eclipse.sensinact.core.model.nexus.emf.EMFUtil;
import org.eclipse.sensinact.core.model.nexus.emf.NamingUtils;
import org.eclipse.sensinact.core.model.nexus.emf.compare.EMFCompareUtil;
import org.eclipse.sensinact.core.notification.NotificationAccumulator;
import org.eclipse.sensinact.core.twin.TimedValue;
Expand Down Expand Up @@ -577,7 +578,7 @@ public EClass createModel(String theModelPackageUri, String modelName, Instant t
throw new IllegalArgumentException("There is an existing model with name " + modelName);
}

String modelClassName = firstToUpper(modelName);
String modelClassName = NamingUtils.sanitizeName(modelName, false);
EPackage ePackage = resourceSet.getPackageRegistry().getEPackage(theModelPackageUri);
if (ePackage == null) {
ePackage = EMFUtil.createPackage(modelName, theModelPackageUri, modelName, resourceSet);
Expand All @@ -590,7 +591,7 @@ public EClass createModel(String theModelPackageUri, String modelName, Instant t

private EReference doCreateService(EClass model, String name, Instant timestamp) {
EPackage ePackage = model.getEPackage();
EClass service = EMFUtil.createEClass(constructServiceEClassName(model.getName(), name), ePackage,
EClass service = EMFUtil.createEClass(NamingUtils.sanitizeName(name, false), ePackage,
(ec) -> createEClassAnnotations(timestamp), ProviderPackage.Literals.SERVICE);
ServiceReference ref = EMFUtil.createServiceReference(model, name, service, true);
EMFUtil.fillMetadata(ref, timestamp, false, name, List.of());
Expand All @@ -610,23 +611,6 @@ private List<EAnnotation> createEClassAnnotations(String model, Instant timestam
EMFUtil.createEAnnotation("model", Map.of("name", model)));
}

/**
* We need a Unique name for the Service Class if they reside in the same
* Package. Thus we create a hopefully unique name.
*
* TODO: Place each Provider in its own Subpackage?
*
* @param providerName
* @param serviceName
* @return
*/
private String constructServiceEClassName(String providerName, String serviceName) {
return firstToUpper(providerName) + firstToUpper(serviceName);
}

private String firstToUpper(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}

public Map<String, Object> getResourceMetadata(Provider provider, EStructuralFeature svcFeature,
final ETypedElement rcFeature) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*********************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation.
*
* 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
*
* Contributors:
* Kentyou - initial implementation
**********************************************************************/
package org.eclipse.sensinact.core.model.nexus.emf;

import java.text.Normalizer;
import java.util.Arrays;
import java.util.stream.Collectors;

/**
* Handles model, provider, service and resource names from the user
*/
public class NamingUtils {

/**
* List of Java keywords according to its specification
*/
public static final String[] keywords = { "abstract", "continue", "for", "new", "switch", "assert", "default", "if",
"package", "synchronized", "boolean", "do", "goto", "private", "this", "break", "double", "implements",
"protected", "throw", "byte", "else", "import", "public", "throws", "case", "enum", "instanceof", "return",
"transient", "catch", "extends", "int", "short", "try", "char", "final", "interface", "static", "void",
"class", "finally", "long", "strictfp", "volatile", "const", "float", "native", "super", "while" };

static {
// Final array can be sorted (in-place sort)
Arrays.sort(keywords);
}

/**
* Checks if the given name is a Java keyword
*
* @param name Name to test
* @return True if the given name is a Java keyword
*/
public static boolean isJavaKeyword(final String name) {
return Arrays.binarySearch(keywords, name) >= 0;
}

/**
* Ensures that the given name is only based on ASCII letters, digits and the
* underscore
*
* @param name Input name
* @param isPath Flag to allow slashes (<code>/</code>) in the name
* @return A name that contains only ASCII letters, digits or underscore, or
* null if the input is empty or null
*/
public static String asciiSanitizeName(final String name, final boolean isPath) {
if (name == null || name.isBlank()) {
return null;
}

if (isPath) {
// Treat each part separately then join everything
return Arrays.stream(name.split("/")).map(p -> asciiSanitizeName(p, false))
.collect(Collectors.joining("/"));
} else {
// Normalize diacritics
final String normalized = Normalizer.normalize(name.strip(), Normalizer.Form.NFKD).replaceAll("\\p{M}", "");
final String sanitized;
if (normalized.isEmpty()) {
// All characters were invalid, create a name with as many underscores as input
// characters
sanitized = name.replaceAll(".", "_");
} else {
// Replace all non acceptable characters with an underscore
sanitized = normalized.replaceAll("[^_A-Za-z0-9]", "_");
}

if (sanitized.isEmpty()) {
return "_";
} else if (!Character.isJavaIdentifierStart(sanitized.charAt(0)) || isJavaKeyword(name)) {
// Make sure we don't start with an invalid character
return "_" + sanitized;
} else {
return sanitized;
}
}
}

public static String firstToUpper(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}

/**
* Ensures the given name is accepted as a Java identifier
*
* @param name Input name
* @param isPath Flag to allow slashes (<code>/</code>) in the name
* @return A name that can be used as Java identifier, or null if the input is
* empty or null
*/
public static String sanitizeName(final String name, final boolean isPath) {
if (name == null || name.isBlank()) {
return null;
}

if (isPath) {
// Treat each part separately then join everything
return Arrays.stream(name.split("/")).map(p -> sanitizeName(p, false)).collect(Collectors.joining("/"));
} else {
// Replace invalid Java identifier letters
final String sanitized = name.strip().chars().mapToObj(
c -> Character.isJavaIdentifierPart(c) || (isPath && c == '/') ? Character.toString((char) c) : "_")
.collect(Collectors.joining());
if (sanitized.isEmpty()) {
return "_";
} else if (!Character.isJavaIdentifierStart(sanitized.charAt(0)) || isJavaKeyword(name)) {
// Make sure we don't start with an invalid character
return "_" + sanitized;
} else {
return firstToUpper(sanitized);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import static java.time.temporal.ChronoUnit.DAYS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
Expand Down Expand Up @@ -67,6 +68,8 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
Expand Down Expand Up @@ -251,7 +254,7 @@ public InputStream createInputStream(URI uri) throws IOException {
assertNotNull(serviceFeature);
assertEquals(TESTSERVICE, serviceFeature.getName());
EClass eClass = (EClass) serviceFeature.getEType();
assertEquals("TestModelTestservice", eClass.getName());
assertEquals("Testservice", eClass.getName());

Service svc = (Service) provider.eGet(serviceFeature);

Expand Down Expand Up @@ -624,6 +627,20 @@ void testUnableToCreateProviderDifferentModelAndClashingId() {
assertThrows(IllegalArgumentException.class,
() -> nexus.createProviderInstance(TEST_PKG, "TestModel2", TESTPROVIDER, now));
}

@ParameterizedTest
@ValueSource(strings = { "test", "123Test", "final", "http://test.de/asldjkhasdlj", "123$.final/-",
"protected", })
void testCreateModelWithStrangeName(String name) {
ModelNexus nexus = new ModelNexus(resourceSet, ProviderPackage.eINSTANCE, () -> accumulator);

nexus.createModel(name, Instant.now());
Provider providerInstance = nexus.createProviderInstance(name, "test", Instant.now());
System.out.println(name + " - " + providerInstance.eClass().getName());
assertNotNull(providerInstance);
assertEquals(name, EMFUtil.getModelName(providerInstance.eClass()));
assertNotEquals(name, providerInstance.eClass().getName());
}
}

@Nested
Expand Down

0 comments on commit df2810d

Please sign in to comment.