Skip to content

Commit

Permalink
Merge pull request #11 from jqassistant-plugin/deserialization-refact…
Browse files Browse the repository at this point in the history
…oring

Deserialization Refactoring
  • Loading branch information
SebastianWendorf authored Nov 1, 2024
2 parents 791b694 + fd19dca commit 3536d3c
Show file tree
Hide file tree
Showing 14 changed files with 391 additions and 121 deletions.
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,19 @@
<artifactId>json</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.buschmais.jqassistant.core</groupId>
<artifactId>test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
@Label("Dependency")
public interface DependencyDescriptor extends NPMDescriptor, NamedDescriptor {

String getDependency();
String getVersionRange();

void setDependency(String dependency);
void setVersionRange(String versionRange);

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
@Label("Engine")
public interface EngineDescriptor extends NPMDescriptor, NamedDescriptor {

String getEngine();
String getVersionRange();

void setEngine(String engine);
void setVersionRange(String versionRange);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package org.jqassistant.plugin.npm.impl;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.jqassistant.plugin.npm.impl.model.Package;
import org.jqassistant.plugin.npm.impl.model.Person;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Manually implements the deserialization of the package.json to allow for arbitrary anomalies
*/
@Slf4j
public class PackageJsonDeserializer extends JsonDeserializer<Package> {

Pattern personPattern = Pattern.compile("([^<>()]+[^ <>()])( <.+>)?( \\(.+\\))?");

@Override
public Package deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);

Package result = new Package();

if(node.isObject()) {
node.fields().forEachRemaining(packageJsonProperty -> {
JsonNode valueNode = packageJsonProperty.getValue();
switch (packageJsonProperty.getKey()) {
case "name": result.setName(deserializeStringProperty("name", valueNode)); break;
case "version": result.setVersion(deserializeStringProperty("version", valueNode)); break;
case "description": result.setDescription(deserializeStringProperty("description", valueNode)); break;
case "keywords": result.setKeywords(deserializeStringArrayProperty("keywords", valueNode)); break;
case "homepage": result.setHomepage(deserializeStringProperty("homepage", valueNode)); break;
case "license": result.setLicense(deserializeStringProperty("license", valueNode)); break;
case "author": result.setAuthor(deserializePersonProperty("author", valueNode)); break;
case "contributors": result.setContributors(deserializeContributorsProperty(valueNode)); break;
case "files": result.setFiles(deserializeStringArrayProperty("files", valueNode)); break;
case "main": result.setMain(deserializeStringProperty("main", valueNode)); break;
case "scripts": result.setScripts(deserializeStringMap("scripts", valueNode)); break;
case "dependencies": result.setDependencies(deserializeStringMap("dependencies", valueNode)); break;
case "devDependencies": result.setDevDependencies(deserializeStringMap("devDependencies", valueNode)); break;
case "peerDependencies": result.setPeerDependencies(deserializeStringMap("peerDependencies", valueNode)); break;
case "engines": result.setEngines(deserializeStringMap("engines", valueNode)); break;
default: log.error("Encountered unknown top-level property in package.json ({})", packageJsonProperty.getKey());
}
});
} else {
log.error("package.json does not contain a top-level object");
}

return result;
}

private String deserializeStringProperty(String propertyName, JsonNode node) {
if(node.isTextual()) {
return node.asText();
} else {
log.error("property {} is not a string", propertyName);
return null;
}
}

private String[] deserializeStringArrayProperty(String propertyName, JsonNode node) {
if(node.isArray()) {
List<String> result = new ArrayList<>();
node.elements().forEachRemaining(element -> {
if(element.isTextual()) {
result.add(element.asText());
} else {
log.error("property {} contains non-string element (skipping)", propertyName);
}
});
return result.toArray(new String[0]);
} else {
log.error("property {} is not a string array", propertyName);
return new String[0];
}
}

private Person deserializePersonProperty(String propertyName, JsonNode node) {
if(node.isTextual()) {
// single string representation, e.g. "John Doe <[email protected]> (https://homepage.com)"
String text = node.asText();
Matcher matcher = personPattern.matcher(text);
if(matcher.matches()) {
String email = matcher.group(2);
String url = matcher.group(3);
Person result = new Person();
result.setName(matcher.group(1));
if(email != null) {
result.setEmail(email.substring(2, email.length() - 1));
}
if(url != null) {
result.setUrl(url.substring(2, url.length() - 1));
}
return result;
} else {
log.error("string content of {} does not match pattern for this property", propertyName);
}
} else if(node.isObject()) {
// object representation
Person result = new Person();
node.fields().forEachRemaining(entry -> {
switch (entry.getKey()) {
case "name": result.setName(deserializeStringProperty(propertyName + ".name", entry.getValue())); break;
case "email": result.setEmail(deserializeStringProperty(propertyName + ".email", entry.getValue())); break;
case "url": result.setUrl(deserializeStringProperty(propertyName + ".url", entry.getValue())); break;
default: log.error("object content of {} does contain unknown property ({})", propertyName, entry.getKey());
}
});
return result;
} else {
log.error("property {} is neither represented through a string nor an object", propertyName);
}
return null;
}

private List<Person> deserializeContributorsProperty(JsonNode node) {
List<Person> result = new ArrayList<>();
if(node.isArray()) {

int index = 0;
for (var it = node.elements(); it.hasNext(); index++) {
JsonNode elem = it.next();
Person p = deserializePersonProperty("contributors[" + index + "]", elem);
if(p != null) {
result.add(p);
}
}
} else {
log.error("property contributors is not an array");
}
return result;
}

private Map<String, String> deserializeStringMap(String propertyName, JsonNode node) {
Map<String, String> result = new HashMap<>();

if(node.isObject()) {
node.fields().forEachRemaining(field -> {
JsonNode value = field.getValue();
if(value.isTextual()) {
result.put(field.getKey(), value.textValue());
} else {
log.error("Property {} of {} is not a string", field.getKey(), propertyName);
}
});
} else {
log.error("property {} is not an object", propertyName);
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.jqassistant.plugin.npm.impl;

import com.buschmais.jqassistant.core.scanner.api.Scanner;
import com.buschmais.jqassistant.core.scanner.api.ScannerPlugin.Requires;
import com.buschmais.jqassistant.core.scanner.api.Scope;
import com.buschmais.jqassistant.plugin.common.api.scanner.AbstractScannerPlugin;
import com.buschmais.jqassistant.plugin.common.api.scanner.filesystem.FileResource;
import com.buschmais.jqassistant.plugin.json.api.model.JSONFileDescriptor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jqassistant.plugin.npm.api.model.PackageDescriptor;
import org.jqassistant.plugin.npm.impl.mapper.PackageMapper;
import org.jqassistant.plugin.npm.impl.model.Package;

import java.io.IOException;

/**
* Scanner plugin for package.json files.
*/
@Requires(JSONFileDescriptor.class)
public class PackageJsonScannerPlugin extends AbstractScannerPlugin<FileResource, PackageDescriptor> {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

@Override
public boolean accepts(FileResource fileResource, String path, Scope scope) {
return path.endsWith("/package.json");
}

@Override
public PackageDescriptor scan(FileResource fileResource, String path, Scope scope, Scanner scanner) throws IOException {
Package value = OBJECT_MAPPER.readValue(fileResource.createStream(), Package.class);
return PackageMapper.INSTANCE.toDescriptor(value, scanner);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.jqassistant.plugin.npm.impl.mapper;

import com.buschmais.jqassistant.core.scanner.api.Scanner;
import com.buschmais.jqassistant.core.store.api.Store;
import com.buschmais.jqassistant.plugin.common.api.mapper.DescriptorMapper;
import com.buschmais.jqassistant.plugin.common.api.model.NamedDescriptor;
import com.buschmais.jqassistant.plugin.json.api.model.JSONFileDescriptor;
import org.jqassistant.plugin.npm.api.model.DependencyDescriptor;
import org.jqassistant.plugin.npm.api.model.EngineDescriptor;
import org.jqassistant.plugin.npm.api.model.PackageDescriptor;
import org.jqassistant.plugin.npm.api.model.ScriptDescriptor;
import org.jqassistant.plugin.npm.impl.model.Package;
import org.mapstruct.*;

import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.mapstruct.factory.Mappers.getMapper;

@Mapper(uses = {PersonMapper.class})
public interface PackageMapper extends DescriptorMapper<Package, PackageDescriptor> {

PackageMapper INSTANCE = getMapper(PackageMapper.class);

@Override
@Mapping(source = "scripts", target = "scripts", qualifiedByName = "scriptsMapping")
@Mapping(source = "dependencies", target = "dependencies", qualifiedByName = "dependencyMapping")
@Mapping(source = "devDependencies", target = "devDependencies", qualifiedByName = "dependencyMapping")
@Mapping(source = "peerDependencies", target = "peerDependencies", qualifiedByName = "dependencyMapping")
@Mapping(source = "engines", target = "engines", qualifiedByName = "engineMapping")
PackageDescriptor toDescriptor(Package value, @Context Scanner scanner);

@Named("scriptsMapping")
default List<ScriptDescriptor> scriptsMapping(Map<String, String> sourceField, @Context Scanner scanner) {
return mapMapProperty(sourceField, ScriptDescriptor.class, ScriptDescriptor::setScript, scanner);
}

@Named("dependencyMapping")
default List<DependencyDescriptor> dependencyMapping(Map<String, String> sourceField, @Context Scanner scanner) {
return mapMapProperty(sourceField, DependencyDescriptor.class, DependencyDescriptor::setVersionRange, scanner);
}

@Named("engineMapping")
default List<EngineDescriptor> engineMapping(Map<String, String> sourceField, @Context Scanner scanner) {
return mapMapProperty(sourceField, EngineDescriptor.class, EngineDescriptor::setVersionRange, scanner);
}

static <T extends NamedDescriptor> List<T> mapMapProperty(Map<String, String> map, Class<T> descriptorType, BiConsumer<T, String> valueConsumer, Scanner scanner) {
if (map != null) {
Store store = scanner.getContext().getStore();
return map.entrySet()
.stream()
.map(entry -> {
T descriptor = store.create(descriptorType);
descriptor.setName(entry.getKey());
valueConsumer.accept(descriptor, entry.getValue());
return descriptor;
})
.collect(toList());
}
return emptyList();
}

@Override
@ObjectFactory
default PackageDescriptor resolve(Package value, @TargetType Class<PackageDescriptor> descriptorType, @Context Scanner scanner) {
JSONFileDescriptor jsonFileDescriptor = scanner.getContext()
.peek(JSONFileDescriptor.class);
Store store = scanner.getContext()
.getStore();
return store.addDescriptorType(jsonFileDescriptor, PackageDescriptor.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.jqassistant.plugin.npm.impl.mapper;

import com.buschmais.jqassistant.core.scanner.api.Scanner;
import com.buschmais.jqassistant.plugin.common.api.mapper.DescriptorMapper;
import org.jqassistant.plugin.npm.api.model.PersonDescriptor;
import org.jqassistant.plugin.npm.impl.model.Person;
import org.mapstruct.Context;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper
public interface PersonMapper extends DescriptorMapper<Person, PersonDescriptor> {

@Override
PersonDescriptor toDescriptor(Person value, @Context Scanner scanner);


List<PersonDescriptor> mapList(List<Person> value, @Context Scanner scanner);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package org.jqassistant.plugin.npm.impl.scanner;
package org.jqassistant.plugin.npm.impl.model;

import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.jqassistant.plugin.npm.impl.PackageJsonDeserializer;

/**
* Used for unmarshalling package.json files using Jackson.
*/
@Getter
@Setter
@ToString
@JsonDeserialize(using = PackageJsonDeserializer.class)
public class Package {

private String name;
Expand Down Expand Up @@ -44,13 +47,4 @@ public class Package {

private Map<String, String> engines;

@Getter
@Setter
@ToString
public static class Person {
private String name;
private String email;
private String url;

}
}
14 changes: 14 additions & 0 deletions src/main/java/org/jqassistant/plugin/npm/impl/model/Person.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.jqassistant.plugin.npm.impl.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Person {
private String name;
private String email;
private String url;
}
Loading

0 comments on commit 3536d3c

Please sign in to comment.