/*
* Copyright 2003-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.generator.impl;
import jetbrains.mps.generator.IGeneratorLogger.ProblemDescription;
import jetbrains.mps.smodel.SNodeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SAbstractLink;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SNode;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Role validation logic extracted out from AbstractTemplateGenerator
* @author Artem Tikhomirov
*/
public class RoleValidation {
private final boolean myShowBadChildWarning;
private final RoleValidator mySuccessValidator = new RoleValidator();
// the code might need refactoring to be more thread-friendly, e.g. validators per thread, not per single generator as it's now
private final ConcurrentHashMap<SConcept, Map<SAbstractLink, RoleValidator>> validators = new ConcurrentHashMap<>();
public RoleValidation(boolean showBadChildWarning) {
myShowBadChildWarning = showBadChildWarning;
}
RoleValidator getValidator(SNode sourceNode, SReferenceLink role) {
final SConcept srcConcept = sourceNode.getConcept();
Map<SAbstractLink, RoleValidator> vmap = getCached(srcConcept);
RoleValidator validator = vmap.get(role);
if (validator != null) {
return validator;
}
if (!srcConcept.isSubConceptOf(role.getOwner())) {
String msg = String.format("Source node of %s concept could not have reference %s (from %s)", srcConcept.getQualifiedName(), role.getName(), role.getOwner().getQualifiedName());
Status s = new Status(msg);
validator = new RoleValidator(s);
} else {
validator = new AcceptableTargetValidator(role);
}
vmap.put(role, validator);
return validator;
}
RoleValidator getValidator(SNode sourceNode, SContainmentLink role) {
if (SNodeUtil.link_BaseConcept_smodelAttribute.equals(role)) {
return mySuccessValidator;
}
final SConcept srcConcept = sourceNode.getConcept();
Map<SAbstractLink, RoleValidator> vmap = getCached(srcConcept);
RoleValidator validator = vmap.get(role);
if (validator != null) {
return validator;
}
if (!srcConcept.isSubConceptOf(role.getOwner())) {
String msg = String.format("No child role %s (%s) known for source node's %s concept", role.getName(), role.getOwner().getQualifiedName(), srcConcept.getQualifiedName());
Status s = new Status(msg);
validator = new RoleValidator(s);
} else {
if (myShowBadChildWarning) {
validator = new AcceptableTargetValidator(role);
} else {
validator = mySuccessValidator;
}
}
vmap.put(role, validator);
return validator;
}
@NotNull
private Map<SAbstractLink, RoleValidator> getCached(SConcept concept) {
Map<SAbstractLink, RoleValidator> vmap = validators.get(concept);
if (vmap == null) {
Map<SAbstractLink, RoleValidator> existing = validators.putIfAbsent(concept, vmap = new ConcurrentHashMap<>());
if (existing != null) {
vmap = existing;
}
}
return vmap;
}
public static class RoleValidator {
private final Status myStatus;
protected RoleValidator() {
myStatus = null;
}
protected RoleValidator(@NotNull Status status) {
myStatus = status;
}
/**
* @return null if validation succeed
*/
public Status validate(SNode targetNode) {
return myStatus;
}
}
private static class AcceptableTargetValidator extends RoleValidator {
private final SAbstractLink myLink;
private final SAbstractConcept myLinkTarget;
private final boolean myIsContainment;
AcceptableTargetValidator(@NotNull SReferenceLink link) {
myLink = link;
myLinkTarget = link.getTargetConcept();
myIsContainment = false;
}
AcceptableTargetValidator(@NotNull SContainmentLink link) {
myLink = link;
myLinkTarget = link.getTargetConcept();
myIsContainment = true;
}
@Override
public Status validate(SNode targetNode) {
// XXX could keep set of concepts already checked to avoid isSubConceptOf (if expensive)
if (targetNode.getConcept().isSubConceptOf(myLinkTarget)) {
return null;
}
if (myIsContainment && DelayedChanges.isTempNode(targetNode)) {
// temporary child node, ignore
return null;
}
String expected = myLinkTarget.getQualifiedName();
String was = targetNode.getConcept().getQualifiedName();
String relationKind = myIsContainment ? "child" : "referent";
String msg = String.format("%s '%s' is expected for role '%s' but was '%s'", relationKind, expected, myLink.getName(), was);
return new Status(msg, GeneratorUtil.describe(targetNode, relationKind));
}
}
public static class Status {
private final String message;
private final ProblemDescription[] descriptions;
public Status(String message, ProblemDescription... descriptions) {
this.message = message;
this.descriptions = descriptions;
}
public String getMessage(String prefix) {
return String.format("%s: %s", prefix, message);
}
public ProblemDescription[] describe(ProblemDescription... extras) {
return GeneratorUtil.concat(this.descriptions, extras);
}
}
}