Merge branch 'dev' into release_candidate

Change-Id: I289c687d2e3c270690bd1cc9b39b76c6638b907b
diff --git a/org.eclipse.mdm.apicopy/src/main/java/org/eclipse/mdm/apicopy/control/ExportTask.java b/org.eclipse.mdm.apicopy/src/main/java/org/eclipse/mdm/apicopy/control/ExportTask.java
index 7a1d0a6..c35ebd7 100644
--- a/org.eclipse.mdm.apicopy/src/main/java/org/eclipse/mdm/apicopy/control/ExportTask.java
+++ b/org.eclipse.mdm.apicopy/src/main/java/org/eclipse/mdm/apicopy/control/ExportTask.java
@@ -200,7 +200,7 @@
 			persist(transaction, testStepDst);
 
 			TemplateTestStep tpl = entityManagerSrc.loadTemplate(testStepSrc).orElseThrow(() -> new ApiCopyException(
-					"Could not retrieve TemplateTestStep for TestStep + " + testStepSrc.getID()));
+					"Could not retrieve TemplateTestStep for TestStep " + testStepSrc.getID()));
 
 			copyContext(testStepSrc, testStepDst, tpl, transaction);
 
diff --git a/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.html b/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.html
index f3dbe9b..fc93595 100644
--- a/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.html
+++ b/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.html
@@ -64,7 +64,7 @@
             <p-splitButton icon="fa fa-arrow-circle-o-down" class="dropdown" (onClick)="saveBasketWithExtension($event, 'mdm')" (onDropdownClick)="stopEvent($event)" title="{{'basket.mdm-basket.tooltip-download-shopping-basket' | translate }}" [model]="launchers" [disabled]="isDownloadDisabled()"></p-splitButton>
             <div class="fileupload btn btn-mdm" title="{{'basket.mdm-basket.tooltip-upload-shopping-basket' | translate }}" (click)=onUploadClick($event)>
               <span class="fa fa-arrow-circle-o-up"></span>
-              <input title="{{'basket.mdm-basket.tooltip-upload-shopping-basket' | translate }}" class="upload" name="datei" type="file" accept=".json, application/json" id="fileInput" (change)="onUploadChange($event)">
+              <input title="{{'basket.mdm-basket.tooltip-upload-shopping-basket' | translate }}" class="upload" name="datei" type="file" [accept]="allowedExtensions" id="fileInput" (change)="onUploadChange($event)">
             </div>
             <button type="button" type="submit" class="btn btn-mdm" (click)="showLoadModal($event)"  title="{{'basket.mdm-basket.tooltip-open-shopping-basket' | translate }}"><span class="fa fa-folder-open"></span></button>
             <button type="button" type="submit" class="btn btn-mdm btn-outline-secondary" (click)="showSaveModal($event)" title="{{'basket.mdm-basket.tooltip-save-shopping-basket' | translate }}"><span class="fa fa-floppy-o"></span></button>
@@ -88,6 +88,8 @@
   </accordion>
 </div>
 
+<p-confirmDialog header="Information" icon="pi pi-exclamation-triangle" ></p-confirmDialog>
+
 <div bsModal #lgLoadModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="SelectSearchComponents" aria-hidden="true" (keyup.enter)="loadBasket()">
   <div class="modal-dialog modal-md">
     <div class="modal-content">
diff --git a/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.ts b/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.ts
index 195cfd2..c94b34c 100644
--- a/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.ts
+++ b/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.component.ts
@@ -38,6 +38,10 @@
 
 import { TranslateService } from '@ngx-translate/core';
 import { streamTranslate, TRANSLATE } from '../core/mdm-core.module';
+import {ConfirmationService} from 'primeng/api';
+import { Observable } from 'rxjs/Observable';
+import { forkJoin, throwError } from 'rxjs';
+import { map, flatMap } from 'rxjs/operators';
 
 @Component({
   selector: 'mdm-basket',
@@ -49,6 +53,8 @@
   @Output() onSelect = new EventEmitter<Node>();
   @Input() activeNode: Node;
 
+  exportableTypes = ['Project', 'Pool', 'Test', 'TestStep', 'Measurement'];
+
   basketName = '';
   basketContent: SearchResult = new SearchResult();
   basket = 'Warenkorb';
@@ -70,6 +76,7 @@
     {label: 'ATFX', command: (event) => this.saveBasketAsATFX(event.originalEvent)}
   ];
   launchers: MenuItem[] = [];
+  allowedExtensions: string[] = [];
 
   @ViewChild(TableviewComponent)
   tableViewComponent: TableviewComponent;
@@ -88,7 +95,8 @@
               private sanitizer: DomSanitizer,
               private nodeService: NodeService,
               private notificationService: MDMNotificationService,
-              private translateService: TranslateService) {
+              private translateService: TranslateService,
+              private confirmationService: ConfirmationService) {
   }
 
   removeSelected() {
@@ -106,10 +114,16 @@
     ));
 
     this._basketService.getFileExtensions()
-      .map(launchers => launchers.map(l => <MenuItem> {
-        label: l.label,
-        command: (event) => this.saveBasketWithExtension(event.originalEvent, l.extension) }))
-      .subscribe(l => this.launchers = this.fixedLaunchers.concat(l) );
+      .map(launchers => <{ menuItems: MenuItem[], extensions: string[] }>{
+          menuItems: launchers.map((l: { label: string; extension: string; }) =>  <MenuItem> {
+            label: l.label,
+            command: (event) => this.saveBasketWithExtension(event.originalEvent, l.extension) }),
+          extensions: launchers.map((l: { extension: string; }) => '.' + l.extension).join(',')
+      })
+      .subscribe(({menuItems, extensions}) => {
+        this.launchers = this.fixedLaunchers.concat(menuItems);
+        this.allowedExtensions = [ 'application/xml' ].concat(extensions);
+      });
 
     this.setItems(this._basketService.items);
 
@@ -249,11 +263,51 @@
       .subscribe(blob => this.saveAsFile(blob, extension, 'shoppingbasket'));
   }
 
+  isExportable(item: MDMItem) {
+    return this.exportableTypes.indexOf(item.type) >= 0;
+  }
+
   saveBasketAsATFX(event: any) {
     if (event) {
       event.stopPropagation();
     }
-    let downloadContent = new Basket(this.basketName, this._basketService.getItems());
+    const notExportableItems = this._basketService.getItems().filter(i => !this.isExportable(i));
+    const exportableItems = this._basketService.getItems().filter(i => this.isExportable(i)).map(i => i.type).join(', ');
+
+    if (exportableItems.length === 0) {
+      // No exportable item in shopping basket -> show message and cancel export
+      const msg = this.translateService.instant('basket.mdm-basket.msg-atfx-only-supports-types',
+          { types: this.exportableTypes.join(', ') })
+          + ' ' + this.translateService.instant('basket.mdm-basket.msg-only-not-exportable-types-selected',
+          { types: notExportableItems.map(i => i.type).join(', ') });
+
+      this.confirmationService.confirm({
+        message: msg,
+        acceptLabel: 'OK',
+        rejectVisible: false,
+      });
+    } else if (notExportableItems.length > 0) {
+      // shopping basket contains not exportable items -> show message and ask if export should continue
+      const msg = this.translateService.instant('basket.mdm-basket.msg-atfx-only-supports-types',
+          { types: this.exportableTypes.join(', ') })
+          + ' ' + this.translateService.instant('basket.mdm-basket.msg-not-exportable-types-continue',
+          { types: exportableItems });
+
+      this.confirmationService.confirm({
+        message: msg,
+        acceptLabel: 'Yes',
+        rejectLabel: 'No',
+        icon: 'pi pi-question-circle',
+        accept: () => this.exportItemsToAtfx(this._basketService.getItems().filter(i => this.isExportable(i))),
+        rejectVisible: true,
+      });
+    } else {
+      this.exportItemsToAtfx(this._basketService.getItems())
+    }
+  }
+
+  exportItemsToAtfx(basket: MDMItem[]) {
+    let downloadContent = new Basket(this.basketName, basket);
 
     this._basketService.getBasketAsAtfx(downloadContent)
       .map(atfx => new Blob([atfx], { type: 'application/xml' }))
@@ -293,19 +347,67 @@
 
   private onUploadEvent(fileInput: any) {
     if (fileInput.files[0]) {
-      let file = fileInput.files[0];
-      let reader = new FileReader();
-      reader.onloadend = (event) => {
-        let upload: Basket = deserialize(Basket, <string> reader.result);
-        this.loadBasket(upload);
-        fileInput.value = '';
-    };
-    reader.readAsText(file);
+      const readFile = (blob: Blob): Observable<string> => Observable.create(o => {
+        if (!(blob instanceof Blob)) {
+          o.error(new Error('Parameter `blob` must be an instance of Blob.'));
+          return;
+        }
+
+        const fileReader = new FileReader();
+        fileReader.onload = () => o.next(fileReader.result);
+        fileReader.onloadend = () => o.complete();
+        fileReader.onabort = err => o.error(err);
+        fileReader.onerror = err => o.error(err);
+        return fileReader.readAsText(blob);
+      });
+
+      readFile(fileInput.files[0]).pipe(
+        flatMap(xml => this.parseXmlShoppingBasket(xml))
+      ).subscribe(
+        basket => this.loadBasket(basket),
+        err => this.notificationService.notifyWarn('Could not load shopping basket from file.', err)
+      );
+    }
   }
+
+  private parseXmlShoppingBasket(xml: string) {
+    let oParser = new DOMParser();
+    let oDOM = oParser.parseFromString(xml, 'application/xml');
+    // print the name of the root element or error message
+    if (oDOM.documentElement.tagName !== 'shoppingbasket') {
+      if (oDOM.documentElement.innerText) {
+        return throwError(new Error('Error while parsing shopping basket: ' + oDOM.documentElement.innerText));
+      } else {
+        return throwError(new Error('Error while parsing shopping basket: XML is missing shoppingbasket root tag'));
+      }
+    }
+
+    // load the name of the shopping basket
+    let name = '';
+    let nameTags = oDOM.documentElement.getElementsByTagName('name');
+    if (nameTags.length > 0) {
+      name = nameTags[0].textContent;
+    }
+
+    // load the business objectes defined by the resturi values in the xml file
+    let items: Observable<Node[]>[] = [];
+    let uris = oDOM.documentElement.getElementsByTagName('resturi');
+    if (uris.length === 0) {
+      return throwError(new Error('No resturis found!'));
+    }
+    for (let uri of <any>uris) {
+      let url = uri.textContent;
+      items.push(this.nodeService.getNodesByAbsoluteUrl(url));
+    }
+
+    // map business objects to MDMItems and load basket
+    return forkJoin(items).pipe(
+      map(n => n.map(x => new MDMItem(x[0].sourceName, x[0].type, x[0].id))),
+      map(i => new Basket(name, i)));
   }
 
   private addData(rows: Row[]) {
-    this.basketContent.rows.push(...rows);
+    this.basketContent.rows.push(... rows);
     // copy and reasign to get new object refference triggering angular change detection in tableview.
     this.basketContent = Object.assign({}, this.basketContent);
   }
diff --git a/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.module.ts b/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.module.ts
index a78281d..d74cbd6 100644
--- a/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.module.ts
+++ b/org.eclipse.mdm.application/src/main/webapp/src/app/basket/mdm-basket.module.ts
@@ -17,15 +17,17 @@
 import { MDMCoreModule } from '../core/mdm-core.module';
 import { TableViewModule } from '../tableview/tableview.module';
 
-import {SplitButtonModule} from 'primeng/primeng';
+import {SplitButtonModule, ConfirmDialogModule, ConfirmationService} from 'primeng/primeng';
 
 @NgModule({
   imports: [
     MDMCoreModule,
     TableViewModule,
-    SplitButtonModule
+    SplitButtonModule,
+    ConfirmDialogModule,
   ],
   declarations: [ MDMBasketComponent ],
   exports: [ MDMBasketComponent],
+  providers: [ ConfirmationService ],
 })
 export class MDMBasketModule { }
diff --git a/org.eclipse.mdm.application/src/main/webapp/src/app/navigator/node.service.ts b/org.eclipse.mdm.application/src/main/webapp/src/app/navigator/node.service.ts
index f57de6d..cfebb86 100644
--- a/org.eclipse.mdm.application/src/main/webapp/src/app/navigator/node.service.ts
+++ b/org.eclipse.mdm.application/src/main/webapp/src/app/navigator/node.service.ts
@@ -137,6 +137,10 @@
     return this.getNode(this._nodeUrl + url);
   }
 
+  getNodesByAbsoluteUrl(url: string) {
+    return this.getNode(url);
+  }
+
   getNodeByRelation(sourceName: string, type: string, id: string) {
     return this.getNode(this._nodeUrl + '/' + sourceName + '/' + this.typeToUrl(type) + '/' + id);
   }
diff --git a/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/de.json b/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/de.json
index 906aeec..0bd55fb 100644
--- a/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/de.json
+++ b/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/de.json
@@ -57,7 +57,10 @@
 			"tooltip-no-name-set": "Kein Name ausgewählt",
 			"tooltip-open-shopping-basket": "Warenkorb öffnen",
 			"tooltip-save-shopping-basket": "Warenkorb speichern",
-			"tooltip-upload-shopping-basket": "Warenkorb hochladen"
+			"tooltip-upload-shopping-basket": "Warenkorb hochladen",
+			"msg-atfx-only-supports-types": "Der ATFX Export unterstützt nur Elemente vom Typ: {{types}}.",
+			"msg-only-not-exportable-types-selected": "Aktuell sind nur Elemente vom Typ {{types}} selektiert!",
+			"msg-not-exportable-types-continue": "Einige Elemente sind vom Typ {{types}} und werden nicht exportiert. Sollen die übrigen Elemente exportiert werden?"
 		}
 	},
 	"core": {
diff --git a/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/en.json b/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/en.json
index 2ef7534..973741b 100644
--- a/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/en.json
+++ b/org.eclipse.mdm.application/src/main/webapp/src/assets/i18n/en.json
@@ -57,7 +57,10 @@
 			"tooltip-no-name-set": "No name set",
 			"tooltip-open-shopping-basket": "Open shopping basket",
 			"tooltip-save-shopping-basket": "Save shopping basket",
-			"tooltip-upload-shopping-basket": "Upload shopping basket"
+			"tooltip-upload-shopping-basket": "Upload shopping basket",
+			"msg-atfx-only-supports-types": "ATFX export only supports export of types: {{types}}.",
+			"msg-only-not-exportable-types-selected": "Currently only items of type {{types}} are selected!",
+			"msg-not-exportable-types-continue": "Some items are of type {{types}} and will not be exported. Continue exporting supported types?"
 		}
 	},
 	"core": {
diff --git a/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMEntity.java b/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMEntity.java
index 7c722c3..43b972f 100644
--- a/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMEntity.java
+++ b/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMEntity.java
@@ -28,9 +28,8 @@
 import org.eclipse.mdm.businessobjects.utils.Serializer;
 import org.eclipse.mdm.businessobjects.utils.ServiceUtils;
 
-import io.vavr.collection.HashMap;
-
 import io.swagger.v3.oas.annotations.media.Schema;
+import io.vavr.collection.HashMap;
 
 /**
  * MDMEntity (Entity for a business object (contains a list of
@@ -58,8 +57,6 @@
 	/** list of relations to transfer */
 	private List<MDMRelation> relations;
 
-	private String status;
-
 	/**
 	 * Constructor.
 	 * 
@@ -257,10 +254,17 @@
 						io.vavr.collection.List.ofAll(entry._2).map(Deletable::getID).asJava()))
 				// append related entities from the mutable store
 				.appendAll(io.vavr.collection.List.ofAll(core.getMutableStore().getCurrent()).
-				// convert list to map: use simple class name as this is the MDM entity type
-						groupBy(e -> e.getClass().getSimpleName())
+				// convert list to map: use simple class name as this is the MDM entity type in
+				// conjunction with the ContextType
+						groupBy(e -> e.getClass().getSimpleName() + "_" + ServiceUtils.getContextType(e))
 						// create relation for every entry
-						.map(entry -> new MDMRelation(null, MDMRelation.RelationType.MUTABLE, entry._1,
+						.map(entry -> new MDMRelation(null, MDMRelation.RelationType.MUTABLE,
+								entry._2.get(0).getClass().getSimpleName(),
+								ServiceUtils.getContextType(entry._2.get(0)), entry._2.map(Entity::getID).asJava())))
+				.appendAll(io.vavr.collection.List.ofAll(core.getPermanentStore().getCurrent())
+						.groupBy(e -> e.getClass().getSimpleName())
+						// create relation for every entry
+						.map(entry -> new MDMRelation(null, MDMRelation.RelationType.PARENT, entry._1,
 								ServiceUtils.getContextType(entry._2.get(0)), entry._2.map(Entity::getID).asJava())))
 				.asJava();
 	}
diff --git a/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMRelation.java b/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMRelation.java
index ea5b84e..d4ca79e 100644
--- a/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMRelation.java
+++ b/org.eclipse.mdm.businessobjects/src/main/java/org/eclipse/mdm/businessobjects/entity/MDMRelation.java
@@ -38,7 +38,7 @@
 	private List<String> ids;
 
 	public enum RelationType {
-		CHILDREN, MUTABLE;
+		CHILDREN, MUTABLE, PARENT;
 	}
 
 	/**
diff --git a/org.eclipse.mdm.openatfx/build.gradle b/org.eclipse.mdm.openatfx/build.gradle
index 38e4910..5568ded 100644
--- a/org.eclipse.mdm.openatfx/build.gradle
+++ b/org.eclipse.mdm.openatfx/build.gradle
@@ -19,7 +19,7 @@
 
 import de.undercouch.gradle.tasks.download.Download
 
-version '0.7.4'
+def atfxVersion  = '0.7.4'
 description = 'Downloads openATFX and publishes it to the local maven repository'
 
 dependencies {
@@ -29,7 +29,6 @@
 
 	// actually we just need the org.asam.ods.* classes
 	compile "org.eclipse.mdm:org.eclipse.mdm.api.odsadapter:${version}"
-
 	testCompile 'org.apache.commons:commons-math:2.2'
 }
 
@@ -38,24 +37,34 @@
 	exclude group: 'javax.xml.stream', module: 'stax-api'
 }
 
+jar {
+    manifest {
+        attributes(
+            'Implementation-Title'   : 'openATFX',
+            'Implementation-Version' : atfxVersion,
+            'Implementation-URL'     : 'https://sourceforge.net/projects/openatfx'
+        )
+    }
+}
+
 task downloadOpenATFX(type: Download) {
 	acceptAnyCertificate true
 	overwrite false
-	// use mirror subdomain of 'https://sourceforge.net/projects/openatfx/files/openatfx-${version}-jars.zip/download'
+	// use mirror subdomain of 'https://sourceforge.net/projects/openatfx/files/openatfx-${atfxVersion}-jars.zip/download'
 	// because gradle has problems establishing a ssl connection with sourceforge directly
-	src "https://liquidtelecom.dl.sourceforge.net/project/openatfx/openatfx-${version}-jars.zip"
-	dest file("${buildDir}/openatfx-${version}-jars.zip")
+	src "https://liquidtelecom.dl.sourceforge.net/project/openatfx/openatfx-${atfxVersion}-jars.zip"
+	dest file("${buildDir}/openatfx-${atfxVersion}-jars.zip")
 	outputs.file dest
 }
 
 task unzipOpenATFX(dependsOn: downloadOpenATFX, type: Copy) {
 	from zipTree(downloadOpenATFX.dest)
 	into buildDir
-	outputs.dir file("${buildDir}/openatfx-${version}")
+	outputs.dir file("${buildDir}/openatfx-${atfxVersion}")
 }
 
 task copySource(dependsOn: unzipOpenATFX, type: Copy) {
-	from file("${buildDir}/openatfx-${version}/src")
+	from file("${buildDir}/openatfx-${atfxVersion}/src")
 	into 'src'
 }
 
diff --git a/release_notes.md b/release_notes.md
index 2daf0e9..abec736 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -4,6 +4,15 @@
 * [mdmbl Eclipse Git Repositories](http://git.eclipse.org/c/?q=mdmbl)
 * [mdmbl nightly builds - last stable version](http://download.eclipse.org/mdmbl/nightly_master/?d)
 
+## Version 5.1.0 ##
+
+### Bugzilla Bugs fixed ###
+
+* [561163](https://bugs.eclipse.org/bugs/show_bug.cgi?id=561163) - Possible NPE in OdsEntityManager#loadTemplate()
+* [553867](https://bugs.eclipse.org/bugs/show_bug.cgi?id=553867) - Upload shopping basket should use xml format
+* [553699](https://bugs.eclipse.org/bugs/show_bug.cgi?id=553699) - Provide a user friendly error message if unsupported business objects are exported to atfx.
+* [560003](https://bugs.eclipse.org/bugs/show_bug.cgi?id=560003) - Relations in Response not shown correctly
+
 ## Version 5.1.0RC1 ##
 
 New Features: