-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from jqassistant-plugin/deserialization-refact…
…oring Deserialization Refactoring
- Loading branch information
Showing
14 changed files
with
391 additions
and
121 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
src/main/java/org/jqassistant/plugin/npm/impl/PackageJsonDeserializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
src/main/java/org/jqassistant/plugin/npm/impl/PackageJsonScannerPlugin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
src/main/java/org/jqassistant/plugin/npm/impl/mapper/PackageMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/main/java/org/jqassistant/plugin/npm/impl/mapper/PersonMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
src/main/java/org/jqassistant/plugin/npm/impl/model/Person.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.