diff --git a/client/src/store/actions.ts b/client/src/store/actions.ts index d6714c8b..df3b6ffa 100644 --- a/client/src/store/actions.ts +++ b/client/src/store/actions.ts @@ -80,6 +80,8 @@ const actions = { ({type: MessageType.ADD_ARTIFACT, artifactPath, source})), removeArtifact: createMessageAction(({artifactPath}: {artifactPath: ArtifactPath}) => ({type: MessageType.REMOVE_ARTIFACT, artifactPath})), + exportArtifact: createMessageAction(({artifactPath, format}: {artifactPath: ArtifactPath, format: string}) => + ({type: MessageType.EXPORT_ARTIFACT, artifactPath, format})), joinRequest: createMessageAction(({artifactPath}: {artifactPath: ArtifactPath}) => ({type: MessageType.JOIN_REQUEST, artifactPath})), leaveRequest: createMessageAction(({artifactPath}: {artifactPath: ArtifactPath}) => ({type: MessageType.LEAVE_REQUEST, artifactPath})), undo: createMessageAction(() => ({type: MessageType.ERROR})), // TODO diff --git a/client/src/types.ts b/client/src/types.ts index 5fbe49ad..0851fee6 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -12,6 +12,7 @@ export enum MessageType { SET_USER_PROFILE = 'SET_USER_PROFILE', ADD_ARTIFACT = 'ADD_ARTIFACT', REMOVE_ARTIFACT = 'REMOVE_ARTIFACT', + EXPORT_ARTIFACT = 'EXPORT_ARTIFACT', JOIN_REQUEST = 'JOIN_REQUEST', LEAVE_REQUEST = 'LEAVE_REQUEST', INITIALIZE = 'INITIALIZE', diff --git a/server/src/main/java/de/ovgu/spldev/varied/Artifact.java b/server/src/main/java/de/ovgu/spldev/varied/Artifact.java index 941bb7ab..c3ee0722 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/Artifact.java +++ b/server/src/main/java/de/ovgu/spldev/varied/Artifact.java @@ -91,10 +91,6 @@ public static class FeatureModel extends Artifact { this(project, name, () -> FeatureModelUtils.loadFeatureModel(path)); } - FeatureModel(Project project, String name, URL url) throws URISyntaxException { - this(project, name, Paths.get(url.toURI())); - } - FeatureModel(Project project, String name, IFeatureModel initialFeatureModel) { this(project, name, () -> initialFeatureModel); } diff --git a/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java b/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java index 4792d57a..c2c78545 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java +++ b/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java @@ -1,10 +1,14 @@ package de.ovgu.spldev.varied; import de.ovgu.featureide.fm.core.base.IFeatureModel; +import de.ovgu.featureide.fm.core.base.IFeatureModelFactory; +import de.ovgu.featureide.fm.core.base.impl.DefaultFeatureModelFactory; +import de.ovgu.featureide.fm.core.base.impl.FeatureModel; import de.ovgu.spldev.varied.kernel.Kernel; import de.ovgu.spldev.varied.messaging.Api; import de.ovgu.spldev.varied.messaging.Message; import de.ovgu.spldev.varied.util.CollaboratorUtils; +import de.ovgu.spldev.varied.util.FeatureModelUtils; import org.pmw.tinylog.Logger; import java.util.*; @@ -71,6 +75,10 @@ static class FeatureModel extends CollaborativeSession { this.kernel = new Kernel(artifactPath, initialFeatureModel); } + public IFeatureModel toFeatureModel() { + return kernel.toFeatureModel(); + } + private void broadcastResponse(Collaborator collaborator, Object[] involvedSiteIDsAndMessage) { String[] involvedSiteIDs = (String[]) involvedSiteIDsAndMessage[0]; String newMessage = (String) involvedSiteIDsAndMessage[1]; @@ -135,6 +143,12 @@ protected boolean _onMessage(Collaborator collaborator, Message.IDecodable messa return true; } + if (message instanceof Api.ExportArtifact) { + Api.ExportArtifact exportArtifactMessage = (Api.ExportArtifact) message; + exportArtifactMessage.data = FeatureModelUtils.serializeFeatureModel(toFeatureModel(), exportArtifactMessage.format); + collaborator.send(exportArtifactMessage); + } + return false; } diff --git a/server/src/main/java/de/ovgu/spldev/varied/Collaborator.java b/server/src/main/java/de/ovgu/spldev/varied/Collaborator.java index 660b9b92..f3cb9199 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/Collaborator.java +++ b/server/src/main/java/de/ovgu/spldev/varied/Collaborator.java @@ -123,14 +123,10 @@ void onMessage(Message message) throws Message.InvalidMessageException { } String source = ((Api.AddArtifact) message).source; Artifact artifact; - if (source == null) { - try { - artifact = new Artifact.FeatureModel(project, artifactPath.getArtifactName(), - Resources.getResource("examples/" + ProjectManager.EMPTY + ".xml")); - } catch (URISyntaxException e) { - throw new RuntimeException("invalid resource path given"); - } - } else + if (source == null) + artifact = new Artifact.FeatureModel(project, artifactPath.getArtifactName(), + ProjectManager.getResourcePath("examples/" + ProjectManager.EMPTY + ".xml")); + else artifact = new Artifact.FeatureModel(project, artifactPath.getArtifactName(), source); project.addArtifact(artifact); CollaboratorManager.getInstance().broadcast(new Api.AddArtifact(Arrays.asList(artifactPath))); diff --git a/server/src/main/java/de/ovgu/spldev/varied/ProjectManager.java b/server/src/main/java/de/ovgu/spldev/varied/ProjectManager.java index dc1d4fab..3f1ca072 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/ProjectManager.java +++ b/server/src/main/java/de/ovgu/spldev/varied/ProjectManager.java @@ -8,6 +8,8 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; import java.util.Scanner; @@ -98,15 +100,19 @@ void addRemoteArtifact(Project project, String artifactName, String url) { })); } - void addExampleArtifact(Project project, String artifactName) { + public static Path getResourcePath(String fileName) { try { - project.addArtifact(new Artifact.FeatureModel(project, artifactName, - Resources.getResource("examples/" + artifactName + ".xml"))); + return Paths.get(Resources.getResource(fileName).toURI()); } catch (URISyntaxException e) { throw new RuntimeException("invalid resource path given"); } } + void addExampleArtifact(Project project, String artifactName) { + project.addArtifact(new Artifact.FeatureModel(project, artifactName, + getResourcePath("examples/" + artifactName + ".xml"))); + } + public void removeProject(Project project) { Logger.info("removing project {}", project); projects.remove(project.getName().toLowerCase()); diff --git a/server/src/main/java/de/ovgu/spldev/varied/kernel/FeatureModelFormat.java b/server/src/main/java/de/ovgu/spldev/varied/kernel/FeatureModelFormat.java index f7c3a6cb..8fbfac59 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/kernel/FeatureModelFormat.java +++ b/server/src/main/java/de/ovgu/spldev/varied/kernel/FeatureModelFormat.java @@ -1,18 +1,26 @@ package de.ovgu.spldev.varied.kernel; -import clojure.lang.PersistentHashMap; -import de.ovgu.featureide.fm.core.base.FeatureUtils; -import de.ovgu.featureide.fm.core.base.IConstraint; -import de.ovgu.featureide.fm.core.base.IFeature; -import de.ovgu.featureide.fm.core.base.IFeatureModel; +import clojure.lang.*; +import de.ovgu.featureide.fm.core.base.*; +import de.ovgu.featureide.fm.core.base.impl.DefaultFeatureModelFactory; +import de.ovgu.featureide.fm.core.io.UnsupportedModelException; import org.prop4j.*; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import static de.ovgu.featureide.fm.core.io.xml.XMLFeatureModelTags.CONJ; +import static de.ovgu.featureide.fm.core.io.xml.XMLFeatureModelTags.DISJ; +import static de.ovgu.featureide.fm.core.localization.StringTable.NOT; + +// adapted from de.ovgu.featureide.fm.core.io.xml.XmlFeatureModelFormat public class FeatureModelFormat { - public static PersistentHashMap toKernel(IFeatureModel featureModel) { + public static APersistentMap toKernel(IFeatureModel featureModel) { HashMap featureModelMap = new HashMap<>(), featuresMap = new HashMap<>(), constraintsMap = new HashMap<>(); @@ -41,7 +49,7 @@ else if (feature.getStructure().isAlternative()) featureMap.put(Kernel.keyword("description"), description.replace("\r", "")); } else featureMap.put(Kernel.keyword("description"), null); - featuresMap.put(feature.getName(), Kernel.toPersistentHashMap(featureMap)); + featuresMap.put(feature.getName(), Kernel.toPersistentMap(featureMap)); }); for (final IConstraint constraint : featureModel.getConstraints()) { @@ -53,12 +61,12 @@ else if (feature.getStructure().isAlternative()) constraintMap.put(Kernel.keyword("formula"), formulaList.get(0)); constraintMap.put(Kernel.keyword("graveyarded?"), false); constraintsMap.put(de.ovgu.spldev.varied.util.FeatureUtils.getConstraintID(constraint).toString(), - Kernel.toPersistentHashMap(constraintMap)); + Kernel.toPersistentMap(constraintMap)); } - featureModelMap.put(Kernel.keyword("features"), Kernel.toPersistentHashMap(featuresMap)); - featureModelMap.put(Kernel.keyword("constraints"), Kernel.toPersistentHashMap(constraintsMap)); - return Kernel.toPersistentHashMap(featureModelMap); + featureModelMap.put(Kernel.keyword("features"), Kernel.toPersistentMap(featuresMap)); + featureModelMap.put(Kernel.keyword("constraints"), Kernel.toPersistentMap(constraintsMap)); + return Kernel.toPersistentMap(featureModelMap); } private static void createConstraint(ArrayList formulaList, org.prop4j.Node node) { @@ -95,4 +103,105 @@ else if (node instanceof Not) formulaList.add(Kernel.toPersistentVector(op)); } + + public static IFeatureModel toFeatureModel(Object kernelContext) { + IFeatureModelFactory featureModelFactory = DefaultFeatureModelFactory.getInstance(); + IFeatureModel featureModel = featureModelFactory.createFeatureModel(); + + APersistentMap featuresHashMap, constraintsHashMap, childrenCacheHashMap; + try { + APersistentMap contextHashMap = (APersistentMap) kernelContext; + Atom atom = (Atom) contextHashMap.get(Kernel.keyword("combined-effect")); + APersistentMap featureModelHashMap = (APersistentMap) atom.deref(); + featuresHashMap = (APersistentMap) featureModelHashMap.get(Kernel.keyword("features")); + constraintsHashMap = (APersistentMap) featureModelHashMap.get(Kernel.keyword("constraints")); + childrenCacheHashMap = (APersistentMap) featureModelHashMap.get(Kernel.keyword("children-cache")); + } catch (Throwable t) { + throw new RuntimeException("feature model not available in kernel context"); + } + + parseFeatures(featureModelFactory, featureModel, featuresHashMap, childrenCacheHashMap, + (APersistentSet) childrenCacheHashMap.get(null), null); + + for (final Object e : constraintsHashMap) { + IMapEntry entry = (IMapEntry) e; + String constraintID = (String) entry.key(); + APersistentMap constraintHashMap = (APersistentMap) entry.val(); + Object formula = constraintHashMap.get(Kernel.keyword("formula")); + boolean graveyarded = (boolean) constraintHashMap.get(Kernel.keyword("graveyarded?")); + + if (!graveyarded) + try { + featureModel.addConstraint(featureModelFactory.createConstraint(featureModel, parseConstraint(featureModel, formula))); + } catch (GraveyardedFeatureException e1) { + } + } + + return featureModel; + } + + private static void parseFeatures(IFeatureModelFactory featureModelFactory, IFeatureModel featureModel, + APersistentMap featuresHashMap, APersistentMap childrenCacheHashMap, APersistentSet children, IFeature parent) { + for (final Object child : children) { + String featureID = (String) child; + APersistentMap featureHashMap = (APersistentMap) featuresHashMap.get(featureID); + if (featureModel.getFeature(featureID) != null) + throw new RuntimeException("Duplicate entry for feature: " + featureID); + + final IFeature feature = featureModelFactory.createFeature(featureModel, featureID); + String groupType = ((Keyword) featureHashMap.get(Kernel.keyword("group-type"))).getName(); + if (groupType.equals("and")) + feature.getStructure().setAnd(); + else if (groupType.equals("alternative")) + feature.getStructure().setAlternative(); + else if (groupType.equals("or")) + feature.getStructure().setOr(); + feature.getStructure().setMandatory(!((boolean) featureHashMap.get(Kernel.keyword("optional?")))); + feature.getStructure().setAbstract((boolean) featureHashMap.get(Kernel.keyword("abstract?"))); + feature.getStructure().setHidden((boolean) featureHashMap.get(Kernel.keyword("hidden?"))); + String description = (String) featureHashMap.get(Kernel.keyword("description")); + if (description != null && !description.trim().isEmpty()) + feature.getProperty().setDescription(description.replace("\r", "")); + de.ovgu.spldev.varied.util.FeatureUtils.setFeatureName(feature, (String) featureHashMap.get(Kernel.keyword("name"))); + + featureModel.addFeature(feature); + if (parent == null) + featureModel.getStructure().setRoot(feature.getStructure()); + else + parent.getStructure().addChild(feature.getStructure()); + + if (childrenCacheHashMap.get(featureID) != null) + parseFeatures(featureModelFactory, featureModel, featuresHashMap, childrenCacheHashMap, + (APersistentSet) childrenCacheHashMap.get(featureID), feature); + } + } + + private static Node parseConstraint(IFeatureModel featureModel, Object formula) throws GraveyardedFeatureException { + if (formula instanceof String) { + final String featureID = (String) formula; + if (featureModel.getFeature(featureID) != null) + return new Literal(featureID); + else + throw new GraveyardedFeatureException(); + } else if (formula instanceof APersistentVector) { + APersistentVector formulaVector = (APersistentVector) formula; + String op = ((Keyword) formulaVector.get(0)).getName(); + Node child1 = parseConstraint(featureModel, formulaVector.get(1)); + Node child2 = op.equals("not") ? null : parseConstraint(featureModel, formulaVector.get(2)); + if (op.equals("disj")) + return new Or(child1, child2); + else if (op.equals("conj")) + return new And(child1, child2); + else if (op.equals("eq")) + return new Equals(child1, child2); + else if (op.equals("imp")) + return new Implies(child1, child2); + else if (op.equals("not")) + return new Not(child1); + } + return null; + } + + private static class GraveyardedFeatureException extends Throwable { + } } \ No newline at end of file diff --git a/server/src/main/java/de/ovgu/spldev/varied/kernel/Kernel.java b/server/src/main/java/de/ovgu/spldev/varied/kernel/Kernel.java index 3d43f399..8fb741fb 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/kernel/Kernel.java +++ b/server/src/main/java/de/ovgu/spldev/varied/kernel/Kernel.java @@ -30,8 +30,8 @@ static Object keyword(String keywordString) { return Clojure.read(":" + keywordString); } - static PersistentHashMap toPersistentHashMap(HashMap hashMap) { - return (PersistentHashMap) Clojure.var("clojure.core", "into").invoke(PersistentHashMap.EMPTY, hashMap); + static APersistentMap toPersistentMap(HashMap hashMap) { + return (APersistentMap) Clojure.var("clojure.core", "into").invoke(PersistentHashMap.EMPTY, hashMap); } static PersistentVector toPersistentVector(ArrayList arrayList) { @@ -82,6 +82,10 @@ public Kernel(Artifact.Path artifactPath, IFeatureModel initialFeatureModel) { callKernelAtomic("serverInitialize", FeatureModelFormat.toKernel(initialFeatureModel)); } + public IFeatureModel toFeatureModel() { + return FeatureModelFormat.toFeatureModel(context); + } + public String generateHeartbeat() { return (String) callKernelAtomic("serverGenerateHeartbeat"); } diff --git a/server/src/main/java/de/ovgu/spldev/varied/messaging/Api.java b/server/src/main/java/de/ovgu/spldev/varied/messaging/Api.java index 616787a7..260e1a97 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/messaging/Api.java +++ b/server/src/main/java/de/ovgu/spldev/varied/messaging/Api.java @@ -21,6 +21,7 @@ public enum TypeEnum { RESET, ADD_ARTIFACT, REMOVE_ARTIFACT, + EXPORT_ARTIFACT, COLLABORATOR_JOINED, COLLABORATOR_LEFT, SET_USER_PROFILE, @@ -67,6 +68,18 @@ public RemoveArtifact(de.ovgu.spldev.varied.Artifact.Path artifactPath) { } } + public static class ExportArtifact extends Message implements Message.IEncodable, Message.IDecodable { + @Expose + public String format; + + @Expose + public String data; + + public ExportArtifact(de.ovgu.spldev.varied.Artifact.Path artifactPath) { + super(TypeEnum.EXPORT_ARTIFACT, artifactPath); + } + } + public static class CollaboratorJoined extends Message implements Message.IEncodable { @Expose Collaborator collaborator; diff --git a/server/src/main/java/de/ovgu/spldev/varied/util/FeatureModelUtils.java b/server/src/main/java/de/ovgu/spldev/varied/util/FeatureModelUtils.java index 7b456f7f..97ac8298 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/util/FeatureModelUtils.java +++ b/server/src/main/java/de/ovgu/spldev/varied/util/FeatureModelUtils.java @@ -1,7 +1,11 @@ package de.ovgu.spldev.varied.util; +import de.ovgu.featureide.fm.core.ExtensionManager; +import de.ovgu.featureide.fm.core.PluginID; +import de.ovgu.featureide.fm.core.base.IFeature; import de.ovgu.featureide.fm.core.base.IFeatureModel; import de.ovgu.featureide.fm.core.base.impl.FMFormatManager; +import de.ovgu.featureide.fm.core.io.IFeatureModelFormat; import de.ovgu.featureide.fm.core.io.IPersistentFormat; import de.ovgu.featureide.fm.core.io.manager.FeatureModelManager; import org.pmw.tinylog.Logger; @@ -20,6 +24,17 @@ private static void renameFeaturesToFeatureIDs(IFeatureModel featureModel) { FeatureUtils.setConstraintID(constraint, UUID.randomUUID())); } + private static void renameFeatureIDsToFeatures(IFeatureModel featureModel) { + de.ovgu.featureide.fm.core.base.FeatureUtils.getFeatureNames(featureModel).forEach(featureID -> { + IFeature feature = featureModel.getFeature(featureID); + String featureName = FeatureUtils.getFeatureName(feature); + if (!featureModel.getRenamingsManager().renameFeature(featureID, featureName)) + throw new RuntimeException("could not rename feature " + featureID + " to " + featureName); + FeatureUtils.removeFeatureName(feature); + }); + featureModel.getConstraints().forEach(FeatureUtils::removeConstraintID); + } + public static IFeatureModel loadFeatureModel(Path path) { Logger.debug("loading feature model from {}", path); IFeatureModel featureModel = FeatureModelManager.load(path).getObject(); @@ -40,4 +55,23 @@ public static IFeatureModel loadFeatureModel(String source, String fileName) { renameFeaturesToFeatureIDs(featureModel); return featureModel; } + + public static String serializeFeatureModel(IFeatureModel featureModel, String formatName) { + Logger.debug("serializing feature model with format {}", formatName); + if (featureModel == null) + throw new RuntimeException("no feature model given"); + featureModel = featureModel.clone(); + renameFeatureIDsToFeatures(featureModel); + IFeatureModelFormat format; + try { + format = FMFormatManager.getInstance().getFormatById(PluginID.PLUGIN_ID + ".format.fm." + formatName); + } catch (ExtensionManager.NoSuchExtensionException e) { + throw new RuntimeException("invalid feature model format given"); + } + return format.write(featureModel); + } + + public static String serializeFeatureModel(IFeatureModel featureModel) { + return serializeFeatureModel(featureModel, "XmlFeatureModelFormat"); + } } diff --git a/server/src/main/java/de/ovgu/spldev/varied/util/FeatureUtils.java b/server/src/main/java/de/ovgu/spldev/varied/util/FeatureUtils.java index 816c5ecb..68cf417b 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/util/FeatureUtils.java +++ b/server/src/main/java/de/ovgu/spldev/varied/util/FeatureUtils.java @@ -20,6 +20,10 @@ public static void setFeatureName(IFeature feature, String name) { feature.getCustomProperties().set(FeatureUtils.NAME_PROPERTY, IPropertyContainer.Type.STRING, name); } + public static void removeFeatureName(IFeature feature) { + feature.getCustomProperties().remove(FeatureUtils.NAME_PROPERTY); + } + public static UUID getConstraintID(IConstraint constraint) { try { return UUID.fromString(constraint.getName()); @@ -31,4 +35,8 @@ public static UUID getConstraintID(IConstraint constraint) { public static void setConstraintID(IConstraint constraint, UUID id) { constraint.setName(id.toString()); } + + public static void removeConstraintID(IConstraint constraint) { + constraint.setName(null); + } }