Bug 565945: improve character sheet

Change-Id: I7c5c56df5fb9272d939105f09d995bc759577709
diff --git a/plugins/org.eclipse.skills/model/Skills.ecore b/plugins/org.eclipse.skills/model/Skills.ecore
index 4f54954..9fe373d 100644
--- a/plugins/org.eclipse.skills/model/Skills.ecore
+++ b/plugins/org.eclipse.skills/model/Skills.ecore
@@ -36,6 +36,7 @@
     </eOperations>
   </eClassifiers>
   <eClassifiers xsi:type="ecore:EClass" name="Badge">
+    <eOperations name="getImageDescriptor" eType="#//ImageDescriptor"/>
     <eStructuralFeatures xsi:type="ecore:EAttribute" name="title" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
     <eStructuralFeatures xsi:type="ecore:EAttribute" name="imageURL" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
   </eClassifiers>
@@ -50,7 +51,7 @@
     <eOperations name="consume">
       <eParameters name="reward" eType="#//Reward"/>
     </eOperations>
-    <eOperations name="getAvatar" eType="#//ImageData"/>
+    <eOperations name="getAvatar" eType="#//ImageDescriptor"/>
     <eOperations name="getSkill" eType="#//Skill">
       <eParameters name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
     </eOperations>
@@ -166,7 +167,8 @@
     <eLiterals name="EXPERT" value="2" literal="Expert"/>
   </eClassifiers>
   <eClassifiers xsi:type="ecore:EDataType" name="CustomDependencyDefinition" instanceClassName="org.eclipse.skills.dependencies.CustomDependencyDefinition"/>
-  <eClassifiers xsi:type="ecore:EDataType" name="ImageData" instanceClassName="org.eclipse.swt.graphics.ImageData"/>
+  <eClassifiers xsi:type="ecore:EDataType" name="ImageDescriptor" instanceClassName="org.eclipse.jface.resource.ImageDescriptor"
+      serializable="false"/>
   <eClassifiers xsi:type="ecore:EDataType" name="ISkillService" instanceClassName="org.eclipse.skills.service.ISkillService"/>
   <eClassifiers xsi:type="ecore:EClass" name="UserDependency" abstract="true" eSuperTypes="#//Dependency">
     <eStructuralFeatures xsi:type="ecore:EReference" name="user" ordered="false" unique="false"
diff --git a/plugins/org.eclipse.skills/model/Skills.genmodel b/plugins/org.eclipse.skills/model/Skills.genmodel
index 9ef7eb3..27be37f 100644
--- a/plugins/org.eclipse.skills/model/Skills.genmodel
+++ b/plugins/org.eclipse.skills/model/Skills.genmodel
@@ -17,7 +17,7 @@
     </genEnums>
     <genDataTypes ecoreDataType="Skills.ecore#//Date"/>
     <genDataTypes ecoreDataType="Skills.ecore#//CustomDependencyDefinition"/>
-    <genDataTypes ecoreDataType="Skills.ecore#//ImageData"/>
+    <genDataTypes ecoreDataType="Skills.ecore#//ImageDescriptor"/>
     <genDataTypes ecoreDataType="Skills.ecore#//ISkillService"/>
     <genClasses ecoreClass="Skills.ecore#//Task">
       <genFeatures property="None" children="true" createChild="true" ecoreFeature="ecore:EReference Skills.ecore#//Task/description"/>
@@ -46,6 +46,7 @@
     <genClasses ecoreClass="Skills.ecore#//Badge">
       <genFeatures createChild="false" ecoreFeature="ecore:EAttribute Skills.ecore#//Badge/title"/>
       <genFeatures createChild="false" ecoreFeature="ecore:EAttribute Skills.ecore#//Badge/imageURL"/>
+      <genOperations ecoreOperation="Skills.ecore#//Badge/getImageDescriptor"/>
     </genClasses>
     <genClasses ecoreClass="Skills.ecore#//User">
       <genFeatures createChild="false" ecoreFeature="ecore:EAttribute Skills.ecore#//User/name"/>
diff --git a/plugins/org.eclipse.skills/model/model.aird b/plugins/org.eclipse.skills/model/model.aird
index 1a5102a..d98cfc8 100644
--- a/plugins/org.eclipse.skills/model/model.aird
+++ b/plugins/org.eclipse.skills/model/model.aird
@@ -5,7 +5,7 @@
     <semanticResources>http://eclipse.org/skills/1.0.0</semanticResources>
     <ownedViews xmi:type="viewpoint:DView" uid="_m68ZsEwrEeqwHIzbUoB2lQ">
       <viewpoint xmi:type="description:Viewpoint" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']"/>
-      <ownedRepresentationDescriptors xmi:type="viewpoint:DRepresentationDescriptor" uid="_n6l40EwrEeqwHIzbUoB2lQ" name="Skills Model" repPath="#_n4mUwEwrEeqwHIzbUoB2lQ" changeId="89e01c84-0727-4aef-a702-f77eb25b5843">
+      <ownedRepresentationDescriptors xmi:type="viewpoint:DRepresentationDescriptor" uid="_n6l40EwrEeqwHIzbUoB2lQ" name="Skills Model" repPath="#_n4mUwEwrEeqwHIzbUoB2lQ" changeId="4a45e48a-3991-46f2-b065-bb6f40324f4b">
         <description xmi:type="description_1:DiagramDescription" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']"/>
         <target xmi:type="ecore:EPackage" href="Skills.ecore#/"/>
       </ownedRepresentationDescriptors>
@@ -143,6 +143,10 @@
               <styles xmi:type="notation:FontStyle" xmi:id="_rImrRNrpEeq_CZJuNlb8eA" fontName="Cantarell" fontHeight="8"/>
               <layoutConstraint xmi:type="notation:Location" xmi:id="_rImrRdrpEeq_CZJuNlb8eA"/>
             </children>
+            <children xmi:type="notation:Node" xmi:id="_Nd9l0BRZEeu0cKJLM565Cg" type="3010" element="_NdivEBRZEeu0cKJLM565Cg">
+              <styles xmi:type="notation:FontStyle" xmi:id="_Nd9l0RRZEeu0cKJLM565Cg" fontName="Cantarell" fontHeight="8"/>
+              <layoutConstraint xmi:type="notation:Location" xmi:id="_Nd9l0hRZEeu0cKJLM565Cg"/>
+            </children>
             <styles xmi:type="notation:SortingStyle" xmi:id="_rIbsKdrpEeq_CZJuNlb8eA"/>
             <styles xmi:type="notation:FilteringStyle" xmi:id="_rIbsKtrpEeq_CZJuNlb8eA"/>
           </children>
@@ -1338,6 +1342,14 @@
         </ownedStyle>
         <actualMapping xmi:type="description_1:NodeMapping" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@containerMappings[name='EC%20EClass']/@subNodeMappings[name='EC%20EAttribute']"/>
       </ownedElements>
+      <ownedElements xmi:type="diagram:DNodeListElement" uid="_NdivEBRZEeu0cKJLM565Cg" name="getImageDescriptor() : ImageDescriptor" tooltipText="getImageDescriptor() : ImageDescriptor">
+        <target xmi:type="ecore:EOperation" href="Skills.ecore#//Badge/getImageDescriptor"/>
+        <semanticElements xmi:type="ecore:EOperation" href="Skills.ecore#//Badge/getImageDescriptor"/>
+        <ownedStyle xmi:type="diagram:BundledImage" uid="_Ndj9MBRZEeu0cKJLM565Cg" labelAlignment="LEFT">
+          <description xmi:type="style:BundledImageDescription" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@containerMappings[name='EC%20EClass']/@subNodeMappings[name='Operation']/@style"/>
+        </ownedStyle>
+        <actualMapping xmi:type="description_1:NodeMapping" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@containerMappings[name='EC%20EClass']/@subNodeMappings[name='Operation']"/>
+      </ownedElements>
     </ownedDiagramElements>
     <ownedDiagramElements xmi:type="diagram:DNodeList" uid="_rGnHMdrpEeq_CZJuNlb8eA" name="Date" tooltipText="" width="14" height="5">
       <target xmi:type="ecore:EDataType" href="Skills.ecore#//Date"/>
@@ -1415,7 +1427,7 @@
         </ownedStyle>
         <actualMapping xmi:type="description_1:NodeMapping" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@containerMappings[name='EC%20EClass']/@subNodeMappings[name='Operation']"/>
       </ownedElements>
-      <ownedElements xmi:type="diagram:DNodeListElement" uid="_0rCe8OYaEeqiRLW56LILnw" name="getAvatar() : ImageData" tooltipText="getAvatar() : ImageData">
+      <ownedElements xmi:type="diagram:DNodeListElement" uid="_0rCe8OYaEeqiRLW56LILnw" name="getAvatar() : ImageDescriptor" tooltipText="getAvatar() : ImageDescriptor">
         <target xmi:type="ecore:EOperation" href="Skills.ecore#//User/getAvatar"/>
         <semanticElements xmi:type="ecore:EOperation" href="Skills.ecore#//User/getAvatar"/>
         <ownedStyle xmi:type="diagram:BundledImage" uid="_0rDGAOYaEeqiRLW56LILnw" labelAlignment="LEFT">
@@ -2316,9 +2328,9 @@
         <actualMapping xmi:type="description_1:NodeMapping" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@containerMappings[name='EC%20EDataType']/@subNodeMappings[name='EC_DataType_InstanceClassName']"/>
       </ownedElements>
     </ownedDiagramElements>
-    <ownedDiagramElements xmi:type="diagram:DNodeList" uid="_O5dToOYbEeqiRLW56LILnw" name="ImageData" tooltipText="" width="14" height="5">
-      <target xmi:type="ecore:EDataType" href="Skills.ecore#//ImageData"/>
-      <semanticElements xmi:type="ecore:EDataType" href="Skills.ecore#//ImageData"/>
+    <ownedDiagramElements xmi:type="diagram:DNodeList" uid="_O5dToOYbEeqiRLW56LILnw" name="ImageDescriptor" tooltipText="" width="14" height="5">
+      <target xmi:type="ecore:EDataType" href="Skills.ecore#//ImageDescriptor"/>
+      <semanticElements xmi:type="ecore:EDataType" href="Skills.ecore#//ImageDescriptor"/>
       <arrangeConstraints>KEEP_LOCATION</arrangeConstraints>
       <arrangeConstraints>KEEP_SIZE</arrangeConstraints>
       <arrangeConstraints>KEEP_RATIO</arrangeConstraints>
@@ -2326,9 +2338,9 @@
         <description xmi:type="style:FlatContainerStyleDescription" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@containerMappings[name='EC%20EDataType']/@style"/>
       </ownedStyle>
       <actualMapping xmi:type="description_1:ContainerMapping" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@containerMappings[name='EC%20EDataType']"/>
-      <ownedElements xmi:type="diagram:DNodeListElement" uid="_O5vngeYbEeqiRLW56LILnw" name="org.eclipse.swt.graphics.ImageData" tooltipText="">
-        <target xmi:type="ecore:EDataType" href="Skills.ecore#//ImageData"/>
-        <semanticElements xmi:type="ecore:EDataType" href="Skills.ecore#//ImageData"/>
+      <ownedElements xmi:type="diagram:DNodeListElement" uid="_O5vngeYbEeqiRLW56LILnw" name="org.eclipse.jface.resource.ImageDescriptor" tooltipText="">
+        <target xmi:type="ecore:EDataType" href="Skills.ecore#//ImageDescriptor"/>
+        <semanticElements xmi:type="ecore:EDataType" href="Skills.ecore#//ImageDescriptor"/>
         <decorations xmi:type="viewpoint:Decoration" uid="_O5vnguYbEeqiRLW56LILnw">
           <description xmi:type="description:SemanticBasedDecoration" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']/@defaultLayer/@decorationDescriptionsSet/@decorationDescriptions[name='External']"/>
         </decorations>
diff --git a/plugins/org.eclipse.skills/plugin.xml b/plugins/org.eclipse.skills/plugin.xml
index 8abfc0c..e23d36a 100644
--- a/plugins/org.eclipse.skills/plugin.xml
+++ b/plugins/org.eclipse.skills/plugin.xml
@@ -38,7 +38,7 @@
       </category>

       <view

             category="org.eclipse.skills.category"

-            class="org.eclipse.skills.ui.views.character.CharacterSheet"

+            class="org.eclipse.skills.ui.views.character.CharacterView"

             id="org.eclipse.skills.view.characterSheet"

             name="Character Sheet"

             restorable="true">

diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IBadge.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IBadge.java
index c9e2680..5252a25 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IBadge.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IBadge.java
@@ -2,9 +2,8 @@
  */
 package org.eclipse.skills.model;
 
-import java.io.IOException;
-
 import org.eclipse.emf.ecore.EObject;
+import org.eclipse.jface.resource.ImageDescriptor;
 
 /**
  * <!-- begin-user-doc --> A representation of the model object '<em><b>Badge</b></em>'. <!-- end-user-doc -->
@@ -63,10 +62,11 @@
 	void setImageURL(String value);
 
 	/**
-	 * @throws IOException
-	 *             on read errors
-	 * @generated NOT
+	 * <!-- begin-user-doc -->
+	 * <!-- end-user-doc -->
+	 * @model kind="operation" dataType="org.eclipse.skills.model.ImageDescriptor"
+	 * @generated
 	 */
-	byte[] getImageData() throws IOException;
+	ImageDescriptor getImageDescriptor();
 
 } // IBadge
diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/ISkillsPackage.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/ISkillsPackage.java
index 55e0c29..27aadec 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/ISkillsPackage.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/ISkillsPackage.java
@@ -307,13 +307,22 @@
 	int BADGE_FEATURE_COUNT = 2;
 
 	/**
+	 * The operation id for the '<em>Get Image Descriptor</em>' operation.
+	 * <!-- begin-user-doc -->
+	 * <!-- end-user-doc -->
+	 * @generated
+	 * @ordered
+	 */
+	int BADGE___GET_IMAGE_DESCRIPTOR = 0;
+
+	/**
 	 * The number of operations of the '<em>Badge</em>' class.
 	 * <!-- begin-user-doc -->
 	 * <!-- end-user-doc -->
 	 * @generated
 	 * @ordered
 	 */
-	int BADGE_OPERATION_COUNT = 0;
+	int BADGE_OPERATION_COUNT = 1;
 
 	/**
 	 * The meta object id for the '{@link org.eclipse.skills.model.impl.MUser <em>User</em>}' class.
@@ -2000,15 +2009,14 @@
 
 
 	/**
-	 * The meta object id for the '<em>Image Data</em>' data type.
+	 * The meta object id for the '<em>Image Descriptor</em>' data type.
 	 * <!-- begin-user-doc -->
 	 * <!-- end-user-doc -->
-	 * @see org.eclipse.swt.graphics.ImageData
-	 * @see org.eclipse.skills.model.impl.MSkillsPackage#getImageData()
+	 * @see org.eclipse.jface.resource.ImageDescriptor
+	 * @see org.eclipse.skills.model.impl.MSkillsPackage#getImageDescriptor()
 	 * @generated
 	 */
-	int IMAGE_DATA = 30;
-
+	int IMAGE_DESCRIPTOR = 30;
 
 	/**
 	 * The meta object id for the '<em>ISkill Service</em>' data type.
@@ -2235,6 +2243,16 @@
 	EAttribute getBadge_ImageURL();
 
 	/**
+	 * Returns the meta object for the '{@link org.eclipse.skills.model.IBadge#getImageDescriptor() <em>Get Image Descriptor</em>}' operation.
+	 * <!-- begin-user-doc -->
+	 * <!-- end-user-doc -->
+	 * @return the meta object for the '<em>Get Image Descriptor</em>' operation.
+	 * @see org.eclipse.skills.model.IBadge#getImageDescriptor()
+	 * @generated
+	 */
+	EOperation getBadge__GetImageDescriptor();
+
+	/**
 	 * Returns the meta object for class '{@link org.eclipse.skills.model.IUser <em>User</em>}'.
 	 * <!-- begin-user-doc -->
 	 * <!-- end-user-doc -->
@@ -3096,15 +3114,15 @@
 	EDataType getCustomDependencyDefinition();
 
 	/**
-	 * Returns the meta object for data type '{@link org.eclipse.swt.graphics.ImageData <em>Image Data</em>}'.
+	 * Returns the meta object for data type '{@link org.eclipse.jface.resource.ImageDescriptor <em>Image Descriptor</em>}'.
 	 * <!-- begin-user-doc -->
 	 * <!-- end-user-doc -->
-	 * @return the meta object for data type '<em>Image Data</em>'.
-	 * @see org.eclipse.swt.graphics.ImageData
-	 * @model instanceClass="org.eclipse.swt.graphics.ImageData"
+	 * @return the meta object for data type '<em>Image Descriptor</em>'.
+	 * @see org.eclipse.jface.resource.ImageDescriptor
+	 * @model instanceClass="org.eclipse.jface.resource.ImageDescriptor" serializeable="false"
 	 * @generated
 	 */
-	EDataType getImageData();
+	EDataType getImageDescriptor();
 
 	/**
 	 * Returns the meta object for data type '{@link org.eclipse.skills.service.ISkillService <em>ISkill Service</em>}'.
@@ -3309,6 +3327,14 @@
 		EAttribute BADGE__IMAGE_URL = eINSTANCE.getBadge_ImageURL();
 
 		/**
+		 * The meta object literal for the '<em><b>Get Image Descriptor</b></em>' operation.
+		 * <!-- begin-user-doc -->
+		 * <!-- end-user-doc -->
+		 * @generated
+		 */
+		EOperation BADGE___GET_IMAGE_DESCRIPTOR = eINSTANCE.getBadge__GetImageDescriptor();
+
+		/**
 		 * The meta object literal for the '{@link org.eclipse.skills.model.impl.MUser <em>User</em>}' class.
 		 * <!-- begin-user-doc -->
 		 * <!-- end-user-doc -->
@@ -4017,14 +4043,14 @@
 		EDataType CUSTOM_DEPENDENCY_DEFINITION = eINSTANCE.getCustomDependencyDefinition();
 
 		/**
-		 * The meta object literal for the '<em>Image Data</em>' data type.
+		 * The meta object literal for the '<em>Image Descriptor</em>' data type.
 		 * <!-- begin-user-doc -->
 		 * <!-- end-user-doc -->
-		 * @see org.eclipse.swt.graphics.ImageData
-		 * @see org.eclipse.skills.model.impl.MSkillsPackage#getImageData()
+		 * @see org.eclipse.jface.resource.ImageDescriptor
+		 * @see org.eclipse.skills.model.impl.MSkillsPackage#getImageDescriptor()
 		 * @generated
 		 */
-		EDataType IMAGE_DATA = eINSTANCE.getImageData();
+		EDataType IMAGE_DESCRIPTOR = eINSTANCE.getImageDescriptor();
 
 		/**
 		 * The meta object literal for the '<em>ISkill Service</em>' data type.
diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IUser.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IUser.java
index f177cef..786adf9 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IUser.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/IUser.java
@@ -5,7 +5,7 @@
 import org.eclipse.emf.common.util.EList;
 
 import org.eclipse.emf.ecore.EObject;
-import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.jface.resource.ImageDescriptor;
 
 /**
  * <!-- begin-user-doc -->
@@ -158,10 +158,10 @@
 	/**
 	 * <!-- begin-user-doc -->
 	 * <!-- end-user-doc -->
-	 * @model kind="operation" dataType="org.eclipse.skills.model.ImageData"
+	 * @model kind="operation" dataType="org.eclipse.skills.model.ImageDescriptor"
 	 * @generated
 	 */
-	ImageData getAvatar();
+	ImageDescriptor getAvatar();
 
 	/**
 	 * <!-- begin-user-doc -->
diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadge.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadge.java
index d2e6675..232338d 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadge.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadge.java
@@ -2,15 +2,17 @@
  */
 package org.eclipse.skills.model.impl;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
 import java.net.URL;
 
 import org.eclipse.emf.common.notify.Notification;
+import org.eclipse.emf.common.util.EList;
 import org.eclipse.emf.ecore.EClass;
 import org.eclipse.emf.ecore.impl.ENotificationImpl;
 import org.eclipse.emf.ecore.impl.MinimalEObjectImpl;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.skills.Activator;
+import org.eclipse.skills.Logger;
 import org.eclipse.skills.model.IBadge;
 import org.eclipse.skills.model.ISkillsPackage;
 
@@ -20,16 +22,16 @@
  * The following features are implemented:
  * </p>
  * <ul>
- *   <li>{@link org.eclipse.skills.model.impl.MBadge#getTitle <em>Title</em>}</li>
- *   <li>{@link org.eclipse.skills.model.impl.MBadge#getImageURL <em>Image URL</em>}</li>
+ * <li>{@link org.eclipse.skills.model.impl.MBadge#getTitle <em>Title</em>}</li>
+ * <li>{@link org.eclipse.skills.model.impl.MBadge#getImageURL <em>Image URL</em>}</li>
  * </ul>
  *
  * @generated
  */
 public class MBadge extends MinimalEObjectImpl.Container implements IBadge {
 	/**
-	 * The default value of the '{@link #getTitle() <em>Title</em>}' attribute.
-	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 * The default value of the '{@link #getTitle() <em>Title</em>}' attribute. <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @see #getTitle()
 	 * @generated
 	 * @ordered
@@ -37,8 +39,8 @@
 	protected static final String TITLE_EDEFAULT = null;
 
 	/**
-	 * The cached value of the '{@link #getTitle() <em>Title</em>}' attribute.
-	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 * The cached value of the '{@link #getTitle() <em>Title</em>}' attribute. <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @see #getTitle()
 	 * @generated
 	 * @ordered
@@ -46,8 +48,8 @@
 	protected String title = TITLE_EDEFAULT;
 
 	/**
-	 * The default value of the '{@link #getImageURL() <em>Image URL</em>}' attribute.
-	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 * The default value of the '{@link #getImageURL() <em>Image URL</em>}' attribute. <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @see #getImageURL()
 	 * @generated
 	 * @ordered
@@ -55,8 +57,8 @@
 	protected static final String IMAGE_URL_EDEFAULT = null;
 
 	/**
-	 * The cached value of the '{@link #getImageURL() <em>Image URL</em>}' attribute.
-	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 * The cached value of the '{@link #getImageURL() <em>Image URL</em>}' attribute. <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @see #getImageURL()
 	 * @generated
 	 * @ordered
@@ -65,6 +67,7 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	protected MBadge() {
@@ -73,6 +76,7 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
@@ -82,6 +86,7 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
@@ -91,11 +96,12 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public void setTitle(String newTitle) {
-		String oldTitle = title;
+		final String oldTitle = title;
 		title = newTitle;
 		if (eNotificationRequired())
 			eNotify(new ENotificationImpl(this, Notification.SET, ISkillsPackage.BADGE__TITLE, oldTitle, title));
@@ -103,6 +109,7 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
@@ -112,11 +119,12 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public void setImageURL(String newImageURL) {
-		String oldImageURL = imageURL;
+		final String oldImageURL = imageURL;
 		imageURL = newImageURL;
 		if (eNotificationRequired())
 			eNotify(new ENotificationImpl(this, Notification.SET, ISkillsPackage.BADGE__IMAGE_URL, oldImageURL, imageURL));
@@ -124,77 +132,112 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
+	 * @generated NOT
+	 */
+	@Override
+	public ImageDescriptor getImageDescriptor() {
+		try {
+			return ImageDescriptor.createFromURL(new URL(getImageURL()));
+		} catch (final Exception e) {
+			Logger.warning(Activator.PLUGIN_ID, "Cannot load badge image data from \"" + getImageURL() + "\" for badge \"" + getTitle() + "\"");
+			return ImageDescriptor.getMissingImageDescriptor();
+		}
+	}
+
+	/**
+	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public Object eGet(int featureID, boolean resolve, boolean coreType) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE__TITLE:
-				return getTitle();
-			case ISkillsPackage.BADGE__IMAGE_URL:
-				return getImageURL();
+		case ISkillsPackage.BADGE__TITLE:
+			return getTitle();
+		case ISkillsPackage.BADGE__IMAGE_URL:
+			return getImageURL();
 		}
 		return super.eGet(featureID, resolve, coreType);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public void eSet(int featureID, Object newValue) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE__TITLE:
-				setTitle((String)newValue);
-				return;
-			case ISkillsPackage.BADGE__IMAGE_URL:
-				setImageURL((String)newValue);
-				return;
+		case ISkillsPackage.BADGE__TITLE:
+			setTitle((String) newValue);
+			return;
+		case ISkillsPackage.BADGE__IMAGE_URL:
+			setImageURL((String) newValue);
+			return;
 		}
 		super.eSet(featureID, newValue);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public void eUnset(int featureID) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE__TITLE:
-				setTitle(TITLE_EDEFAULT);
-				return;
-			case ISkillsPackage.BADGE__IMAGE_URL:
-				setImageURL(IMAGE_URL_EDEFAULT);
-				return;
+		case ISkillsPackage.BADGE__TITLE:
+			setTitle(TITLE_EDEFAULT);
+			return;
+		case ISkillsPackage.BADGE__IMAGE_URL:
+			setImageURL(IMAGE_URL_EDEFAULT);
+			return;
 		}
 		super.eUnset(featureID);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public boolean eIsSet(int featureID) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE__TITLE:
-				return TITLE_EDEFAULT == null ? title != null : !TITLE_EDEFAULT.equals(title);
-			case ISkillsPackage.BADGE__IMAGE_URL:
-				return IMAGE_URL_EDEFAULT == null ? imageURL != null : !IMAGE_URL_EDEFAULT.equals(imageURL);
+		case ISkillsPackage.BADGE__TITLE:
+			return TITLE_EDEFAULT == null ? title != null : !TITLE_EDEFAULT.equals(title);
+		case ISkillsPackage.BADGE__IMAGE_URL:
+			return IMAGE_URL_EDEFAULT == null ? imageURL != null : !IMAGE_URL_EDEFAULT.equals(imageURL);
 		}
 		return super.eIsSet(featureID);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
+	 * @generated
+	 */
+	@Override
+	public Object eInvoke(int operationID, EList<?> arguments) throws InvocationTargetException {
+		switch (operationID) {
+		case ISkillsPackage.BADGE___GET_IMAGE_DESCRIPTOR:
+			return getImageDescriptor();
+		}
+		return super.eInvoke(operationID, arguments);
+	}
+
+	/**
+	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public String toString() {
-		if (eIsProxy()) return super.toString();
+		if (eIsProxy())
+			return super.toString();
 
-		StringBuilder result = new StringBuilder(super.toString());
+		final StringBuilder result = new StringBuilder(super.toString());
 		result.append(" (title: ");
 		result.append(title);
 		result.append(", imageURL: ");
@@ -202,24 +245,4 @@
 		result.append(')');
 		return result.toString();
 	}
-
-	/**
-	 * @generated NOT
-	 */
-	@Override
-	public byte[] getImageData() throws IOException {
-
-		final byte[] buffer = new byte[8 * 1024];
-		final ByteArrayOutputStream data = new ByteArrayOutputStream();
-		try (InputStream in = new URL(getImageURL()).openStream()) {
-			int chars = in.read(buffer);
-			while (chars > 0) {
-				data.write(buffer, 0, chars);
-				chars = in.read(buffer);
-			}
-		}
-
-		return data.toByteArray();
-	}
-
 } // MBadge
diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadgeReward.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadgeReward.java
index d0f3d70..c70ec0d 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadgeReward.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MBadgeReward.java
@@ -27,15 +27,15 @@
  * The following features are implemented:
  * </p>
  * <ul>
- *   <li>{@link org.eclipse.skills.model.impl.MBadgeReward#getBadge <em>Badge</em>}</li>
+ * <li>{@link org.eclipse.skills.model.impl.MBadgeReward#getBadge <em>Badge</em>}</li>
  * </ul>
  *
  * @generated
  */
 public class MBadgeReward extends MinimalEObjectImpl.Container implements IBadgeReward {
 	/**
-	 * The cached value of the '{@link #getBadge() <em>Badge</em>}' containment reference.
-	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 * The cached value of the '{@link #getBadge() <em>Badge</em>}' containment reference. <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @see #getBadge()
 	 * @generated
 	 * @ordered
@@ -44,6 +44,7 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	protected MBadgeReward() {
@@ -52,6 +53,7 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
@@ -61,6 +63,7 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
@@ -70,20 +73,25 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	public NotificationChain basicSetBadge(IBadge newBadge, NotificationChain msgs) {
-		IBadge oldBadge = badge;
+		final IBadge oldBadge = badge;
 		badge = newBadge;
 		if (eNotificationRequired()) {
-			ENotificationImpl notification = new ENotificationImpl(this, Notification.SET, ISkillsPackage.BADGE_REWARD__BADGE, oldBadge, newBadge);
-			if (msgs == null) msgs = notification; else msgs.add(notification);
+			final ENotificationImpl notification = new ENotificationImpl(this, Notification.SET, ISkillsPackage.BADGE_REWARD__BADGE, oldBadge, newBadge);
+			if (msgs == null)
+				msgs = notification;
+			else
+				msgs.add(notification);
 		}
 		return msgs;
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
@@ -91,13 +99,13 @@
 		if (newBadge != badge) {
 			NotificationChain msgs = null;
 			if (badge != null)
-				msgs = ((InternalEObject)badge).eInverseRemove(this, EOPPOSITE_FEATURE_BASE - ISkillsPackage.BADGE_REWARD__BADGE, null, msgs);
+				msgs = ((InternalEObject) badge).eInverseRemove(this, EOPPOSITE_FEATURE_BASE - ISkillsPackage.BADGE_REWARD__BADGE, null, msgs);
 			if (newBadge != null)
-				msgs = ((InternalEObject)newBadge).eInverseAdd(this, EOPPOSITE_FEATURE_BASE - ISkillsPackage.BADGE_REWARD__BADGE, null, msgs);
+				msgs = ((InternalEObject) newBadge).eInverseAdd(this, EOPPOSITE_FEATURE_BASE - ISkillsPackage.BADGE_REWARD__BADGE, null, msgs);
 			msgs = basicSetBadge(newBadge, msgs);
-			if (msgs != null) msgs.dispatch();
-		}
-		else if (eNotificationRequired())
+			if (msgs != null)
+				msgs.dispatch();
+		} else if (eNotificationRequired())
 			eNotify(new ENotificationImpl(this, Notification.SET, ISkillsPackage.BADGE_REWARD__BADGE, newBadge, newBadge));
 	}
 
@@ -112,7 +120,8 @@
 
 		try {
 			// try to clone image to a local folder
-			SkillService.getInstance().storeResource(getBadge().getTitle(), getBadge().getImageData());
+			SkillService.getInstance().storeResource(getBadge().getTitle(), getBadge().getImageDescriptor().getImageData(100).data);
+
 		} catch (final IOException e) {
 			Logger.error(Activator.PLUGIN_ID, "Could not copy badge to local folder", e);
 		}
@@ -122,81 +131,87 @@
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public NotificationChain eInverseRemove(InternalEObject otherEnd, int featureID, NotificationChain msgs) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE_REWARD__BADGE:
-				return basicSetBadge(null, msgs);
+		case ISkillsPackage.BADGE_REWARD__BADGE:
+			return basicSetBadge(null, msgs);
 		}
 		return super.eInverseRemove(otherEnd, featureID, msgs);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public Object eGet(int featureID, boolean resolve, boolean coreType) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE_REWARD__BADGE:
-				return getBadge();
+		case ISkillsPackage.BADGE_REWARD__BADGE:
+			return getBadge();
 		}
 		return super.eGet(featureID, resolve, coreType);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public void eSet(int featureID, Object newValue) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE_REWARD__BADGE:
-				setBadge((IBadge)newValue);
-				return;
+		case ISkillsPackage.BADGE_REWARD__BADGE:
+			setBadge((IBadge) newValue);
+			return;
 		}
 		super.eSet(featureID, newValue);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public void eUnset(int featureID) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE_REWARD__BADGE:
-				setBadge((IBadge)null);
-				return;
+		case ISkillsPackage.BADGE_REWARD__BADGE:
+			setBadge((IBadge) null);
+			return;
 		}
 		super.eUnset(featureID);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public boolean eIsSet(int featureID) {
 		switch (featureID) {
-			case ISkillsPackage.BADGE_REWARD__BADGE:
-				return badge != null;
+		case ISkillsPackage.BADGE_REWARD__BADGE:
+			return badge != null;
 		}
 		return super.eIsSet(featureID);
 	}
 
 	/**
 	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 *
 	 * @generated
 	 */
 	@Override
 	public Object eInvoke(int operationID, EList<?> arguments) throws InvocationTargetException {
 		switch (operationID) {
-			case ISkillsPackage.BADGE_REWARD___PAY_OUT__IUSER:
-				payOut((IUser)arguments.get(0));
-				return null;
+		case ISkillsPackage.BADGE_REWARD___PAY_OUT__IUSER:
+			payOut((IUser) arguments.get(0));
+			return null;
 		}
 		return super.eInvoke(operationID, arguments);
 	}
diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsFactory.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsFactory.java
index 75e967c..b644860 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsFactory.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsFactory.java
@@ -16,7 +16,6 @@
 import org.eclipse.skills.dependencies.CustomDependencyDefinition;
 import org.eclipse.skills.model.*;
 import org.eclipse.skills.service.ISkillService;
-import org.eclipse.swt.graphics.ImageData;
 
 /**
  * <!-- begin-user-doc -->
@@ -102,8 +101,6 @@
 				return createDateFromString(eDataType, initialValue);
 			case ISkillsPackage.CUSTOM_DEPENDENCY_DEFINITION:
 				return createCustomDependencyDefinitionFromString(eDataType, initialValue);
-			case ISkillsPackage.IMAGE_DATA:
-				return createImageDataFromString(eDataType, initialValue);
 			case ISkillsPackage.ISKILL_SERVICE:
 				return createISkillServiceFromString(eDataType, initialValue);
 			default:
@@ -125,8 +122,6 @@
 				return convertDateToString(eDataType, instanceValue);
 			case ISkillsPackage.CUSTOM_DEPENDENCY_DEFINITION:
 				return convertCustomDependencyDefinitionToString(eDataType, instanceValue);
-			case ISkillsPackage.IMAGE_DATA:
-				return convertImageDataToString(eDataType, instanceValue);
 			case ISkillsPackage.ISKILL_SERVICE:
 				return convertISkillServiceToString(eDataType, instanceValue);
 			default:
@@ -426,24 +421,6 @@
 	 * <!-- end-user-doc -->
 	 * @generated
 	 */
-	public ImageData createImageDataFromString(EDataType eDataType, String initialValue) {
-		return (ImageData)super.createFromString(eDataType, initialValue);
-	}
-
-	/**
-	 * <!-- begin-user-doc -->
-	 * <!-- end-user-doc -->
-	 * @generated
-	 */
-	public String convertImageDataToString(EDataType eDataType, Object instanceValue) {
-		return super.convertToString(eDataType, instanceValue);
-	}
-
-	/**
-	 * <!-- begin-user-doc -->
-	 * <!-- end-user-doc -->
-	 * @generated
-	 */
 	public ISkillService createISkillServiceFromString(EDataType eDataType, String initialValue) {
 		return (ISkillService)super.createFromString(eDataType, initialValue);
 	}
diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsPackage.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsPackage.java
index 4bc0379..5689bf8 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsPackage.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MSkillsPackage.java
@@ -14,6 +14,7 @@
 
 import org.eclipse.emf.ecore.impl.EPackageImpl;
 
+import org.eclipse.jface.resource.ImageDescriptor;
 import org.eclipse.skills.dependencies.CustomDependencyDefinition;
 import org.eclipse.skills.model.IAndDependency;
 import org.eclipse.skills.model.IBadge;
@@ -46,7 +47,6 @@
 import org.eclipse.skills.model.IUserTask;
 import org.eclipse.skills.model.LevelName;
 import org.eclipse.skills.service.ISkillService;
-import org.eclipse.swt.graphics.ImageData;
 
 /**
  * <!-- begin-user-doc -->
@@ -270,7 +270,7 @@
 	 * <!-- end-user-doc -->
 	 * @generated
 	 */
-	private EDataType imageDataEDataType = null;
+	private EDataType imageDescriptorEDataType = null;
 
 	/**
 	 * <!-- begin-user-doc -->
@@ -546,6 +546,16 @@
 	 * @generated
 	 */
 	@Override
+	public EOperation getBadge__GetImageDescriptor() {
+		return badgeEClass.getEOperations().get(0);
+	}
+
+	/**
+	 * <!-- begin-user-doc -->
+	 * <!-- end-user-doc -->
+	 * @generated
+	 */
+	@Override
 	public EClass getUser() {
 		return userEClass;
 	}
@@ -1366,8 +1376,8 @@
 	 * @generated
 	 */
 	@Override
-	public EDataType getImageData() {
-		return imageDataEDataType;
+	public EDataType getImageDescriptor() {
+		return imageDescriptorEDataType;
 	}
 
 	/**
@@ -1432,6 +1442,7 @@
 		badgeEClass = createEClass(BADGE);
 		createEAttribute(badgeEClass, BADGE__TITLE);
 		createEAttribute(badgeEClass, BADGE__IMAGE_URL);
+		createEOperation(badgeEClass, BADGE___GET_IMAGE_DESCRIPTOR);
 
 		userEClass = createEClass(USER);
 		createEAttribute(userEClass, USER__NAME);
@@ -1541,7 +1552,7 @@
 		// Create data types
 		dateEDataType = createEDataType(DATE);
 		customDependencyDefinitionEDataType = createEDataType(CUSTOM_DEPENDENCY_DEFINITION);
-		imageDataEDataType = createEDataType(IMAGE_DATA);
+		imageDescriptorEDataType = createEDataType(IMAGE_DESCRIPTOR);
 		iSkillServiceEDataType = createEDataType(ISKILL_SERVICE);
 	}
 
@@ -1621,6 +1632,8 @@
 		initEAttribute(getBadge_Title(), ecorePackage.getEString(), "title", null, 0, 1, IBadge.class, !IS_TRANSIENT, !IS_VOLATILE, IS_CHANGEABLE, !IS_UNSETTABLE, !IS_ID, IS_UNIQUE, !IS_DERIVED, IS_ORDERED);
 		initEAttribute(getBadge_ImageURL(), ecorePackage.getEString(), "imageURL", null, 0, 1, IBadge.class, !IS_TRANSIENT, !IS_VOLATILE, IS_CHANGEABLE, !IS_UNSETTABLE, !IS_ID, IS_UNIQUE, !IS_DERIVED, IS_ORDERED);
 
+		initEOperation(getBadge__GetImageDescriptor(), this.getImageDescriptor(), "getImageDescriptor", 0, 1, IS_UNIQUE, IS_ORDERED);
+
 		initEClass(userEClass, IUser.class, "User", !IS_ABSTRACT, !IS_INTERFACE, IS_GENERATED_INSTANCE_CLASS);
 		initEAttribute(getUser_Name(), ecorePackage.getEString(), "name", null, 0, 1, IUser.class, !IS_TRANSIENT, !IS_VOLATILE, IS_CHANGEABLE, !IS_UNSETTABLE, !IS_ID, IS_UNIQUE, !IS_DERIVED, IS_ORDERED);
 		initEReference(getUser_Skills(), this.getSkill(), null, "skills", null, 0, -1, IUser.class, !IS_TRANSIENT, !IS_VOLATILE, IS_CHANGEABLE, IS_COMPOSITE, !IS_RESOLVE_PROXIES, !IS_UNSETTABLE, IS_UNIQUE, !IS_DERIVED, IS_ORDERED);
@@ -1638,7 +1651,7 @@
 		op = initEOperation(getUser__Consume__IReward(), null, "consume", 0, 1, IS_UNIQUE, IS_ORDERED);
 		addEParameter(op, this.getReward(), "reward", 0, 1, IS_UNIQUE, IS_ORDERED);
 
-		initEOperation(getUser__GetAvatar(), this.getImageData(), "getAvatar", 0, 1, IS_UNIQUE, IS_ORDERED);
+		initEOperation(getUser__GetAvatar(), this.getImageDescriptor(), "getAvatar", 0, 1, IS_UNIQUE, IS_ORDERED);
 
 		op = initEOperation(getUser__GetSkill__String(), this.getSkill(), "getSkill", 0, 1, IS_UNIQUE, IS_ORDERED);
 		addEParameter(op, ecorePackage.getEString(), "name", 0, 1, IS_UNIQUE, IS_ORDERED);
@@ -1756,7 +1769,7 @@
 		// Initialize data types
 		initEDataType(dateEDataType, Date.class, "Date", IS_SERIALIZABLE, !IS_GENERATED_INSTANCE_CLASS);
 		initEDataType(customDependencyDefinitionEDataType, CustomDependencyDefinition.class, "CustomDependencyDefinition", IS_SERIALIZABLE, !IS_GENERATED_INSTANCE_CLASS);
-		initEDataType(imageDataEDataType, ImageData.class, "ImageData", IS_SERIALIZABLE, !IS_GENERATED_INSTANCE_CLASS);
+		initEDataType(imageDescriptorEDataType, ImageDescriptor.class, "ImageDescriptor", !IS_SERIALIZABLE, !IS_GENERATED_INSTANCE_CLASS);
 		initEDataType(iSkillServiceEDataType, ISkillService.class, "ISkillService", IS_SERIALIZABLE, !IS_GENERATED_INSTANCE_CLASS);
 
 		// Create resource
diff --git a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MUser.java b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MUser.java
index 436997f..32462c1 100644
--- a/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MUser.java
+++ b/plugins/org.eclipse.skills/src-gen/org/eclipse/skills/model/impl/MUser.java
@@ -2,8 +2,8 @@
  */
 package org.eclipse.skills.model.impl;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.util.Collection;
 import java.util.Objects;
@@ -19,7 +19,9 @@
 import org.eclipse.emf.ecore.util.EObjectContainmentEList;
 import org.eclipse.emf.ecore.util.EcoreUtil;
 import org.eclipse.emf.ecore.util.InternalEList;
+import org.eclipse.jface.resource.ImageDescriptor;
 import org.eclipse.skills.Activator;
+import org.eclipse.skills.Logger;
 import org.eclipse.skills.model.IBadge;
 import org.eclipse.skills.model.IReward;
 import org.eclipse.skills.model.ISkill;
@@ -303,34 +305,34 @@
 	}
 
 	/**
-	 * <!-- begin-user-doc --> <!-- end-user-doc -->
+	 * Get the image data for the chosen avatar.
 	 *
 	 * @generated NOT
 	 */
 	@Override
-	public ImageData getAvatar() {
-		try {
-			final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
-			if (skillService.hasResource(ISkillService.RESOURCE_AVATAR)) {
-				return new ImageData(new ByteArrayInputStream(skillService.loadResource(ISkillService.RESOURCE_AVATAR)));
+	public ImageDescriptor getAvatar() {
+		final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
+		if (skillService.hasResource(ISkillService.RESOURCE_AVATAR)) {
+			try (InputStream input = skillService.openResource(ISkillService.RESOURCE_AVATAR)) {
+				final ImageData imageData = new ImageData(input);
+				return ImageDescriptor.createFromImageData(imageData);
 
-			} else {
+			} catch (final IOException e) {
+				Logger.warning(Activator.PLUGIN_ID, "Failed to load avatar image", e);
+			}
+
+		} else {
+			try {
 				final byte[] imageData = AvatarCreator.getRandomAvatarData();
 				skillService.storeResource(ISkillService.RESOURCE_AVATAR, imageData);
 
 				return getAvatar();
-			}
-
-		} catch (final IOException e) {
-			// avatar cannot be loaded
-			try {
-				return new ImageData(Activator.getDefault().loadResource("/resources/broken_image.png"));
-			} catch (final IOException e1) {
-				// giving up
-				// FIXME null is bad here
-				return null;
+			} catch (final IOException e) {
+				Logger.warning(Activator.PLUGIN_ID, "Failed to store a new avatar image", e);
 			}
 		}
+
+		return ImageDescriptor.getMissingImageDescriptor();
 	}
 
 	/**
@@ -513,5 +515,4 @@
 		result.append(')');
 		return result.toString();
 	}
-
 } // MUser
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/ISkillService.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/ISkillService.java
index 150eaa2..b410419 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/ISkillService.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/ISkillService.java
@@ -20,6 +20,7 @@
 import org.eclipse.skills.model.ITask;
 import org.eclipse.skills.model.IUser;
 import org.eclipse.skills.model.IUserTask;
+import org.eclipse.skills.service.storage.IDataStorage;
 
 /**
  * Global service to manage user statistics, available quests and skills. To get the service instance use
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/SkillService.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/SkillService.java
index 97eb38f..5f0dd6f 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/SkillService.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/SkillService.java
@@ -35,6 +35,7 @@
 import org.eclipse.skills.model.ITask;
 import org.eclipse.skills.model.IUser;
 import org.eclipse.skills.model.IUserTask;
+import org.eclipse.skills.service.storage.DataStorageProxy;
 import org.eclipse.skills.service.storage.WorkspaceDataStorage;
 import org.eclipse.ui.PlatformUI;
 import org.osgi.service.event.Event;
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/UserStorage.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/UserStorage.java
index f4e1395..4b310d6 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/UserStorage.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/UserStorage.java
@@ -31,6 +31,8 @@
 import org.eclipse.skills.Logger;
 import org.eclipse.skills.model.ISkillsPackage;
 import org.eclipse.skills.model.IUser;
+import org.eclipse.skills.service.storage.DataStorageProxy;
+import org.eclipse.skills.service.storage.IDataStorage;
 
 public class UserStorage extends DataStorageProxy {
 
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/DataStorageProxy.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/DataStorageProxy.java
similarity index 82%
rename from plugins/org.eclipse.skills/src/org/eclipse/skills/service/DataStorageProxy.java
rename to plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/DataStorageProxy.java
index e3fbb11..f78e4aa 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/DataStorageProxy.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/DataStorageProxy.java
@@ -11,10 +11,11 @@
  *     Christian Pontesegger - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.skills.service;
+package org.eclipse.skills.service.storage;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
 
 public class DataStorageProxy implements IDataStorage {
 
@@ -52,4 +53,12 @@
 
 		fBaseStorage = baseStorage;
 	}
+
+	@Override
+	public InputStream openResource(String name) throws IOException {
+		if (hasResource(name))
+			return getStorage().openResource(name);
+
+		throw new FileNotFoundException(String.format("Resource \"%s\" could not be found", name));
+	}
 }
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/IDataStorage.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/IDataStorage.java
similarity index 79%
rename from plugins/org.eclipse.skills/src/org/eclipse/skills/service/IDataStorage.java
rename to plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/IDataStorage.java
index 358cb42..3371d2b 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/IDataStorage.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/IDataStorage.java
@@ -11,9 +11,10 @@
  *     Christian Pontesegger - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.skills.service;
+package org.eclipse.skills.service.storage;
 
 import java.io.IOException;
+import java.io.InputStream;
 
 public interface IDataStorage {
 
@@ -38,10 +39,22 @@
 	 *            name of resource in storage
 	 * @throws IOException
 	 *             when data cannot be loaded
+	 * @return resource binary data
 	 */
 	public byte[] loadResource(String name) throws IOException;
 
 	/**
+	 * Get an input stream to a resource.
+	 *
+	 * @param name
+	 *            name of resource in storage
+	 * @throws IOException
+	 *             when data cannot be loaded
+	 * @return resource input stream
+	 */
+	InputStream openResource(String name) throws IOException;
+
+	/**
 	 * Check if a resource exists in the storage.
 	 *
 	 * @param name
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/WorkspaceDataStorage.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/WorkspaceDataStorage.java
index db6e27a..d8cb415 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/WorkspaceDataStorage.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/service/storage/WorkspaceDataStorage.java
@@ -14,7 +14,9 @@
 package org.eclipse.skills.service.storage;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -22,7 +24,6 @@
 
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.skills.Activator;
-import org.eclipse.skills.service.IDataStorage;
 
 public class WorkspaceDataStorage implements IDataStorage {
 
@@ -34,7 +35,7 @@
 	@Override
 	public void storeResource(String name, byte[] data) throws IOException {
 		final Path target = Paths.get(getUserStorageLocation().getAbsolutePath(), name);
-		Files.write(target, data, StandardOpenOption.CREATE);
+		Files.write(target, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
 	}
 
 	@Override
@@ -44,6 +45,12 @@
 	}
 
 	@Override
+	public InputStream openResource(String name) throws IOException {
+		final Path target = Paths.get(getUserStorageLocation().getAbsolutePath(), name);
+		return new FileInputStream(target.toFile());
+	}
+
+	@Override
 	public boolean hasResource(String name) {
 		final Path target = Paths.get(getUserStorageLocation().getAbsolutePath(), name);
 		return target.toFile().exists();
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/BadgesComposite.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/BadgesComposite.java
index 7be92ac..48febc1 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/BadgesComposite.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/BadgesComposite.java
@@ -13,39 +13,22 @@
 
 package org.eclipse.skills.ui.views.character;
 
-import java.util.Collection;
-
+import org.eclipse.jface.resource.ResourceManager;
 import org.eclipse.skills.model.IBadge;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.layout.FillLayout;
 import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Label;
-import org.eclipse.wb.swt.SWTResourceManager;
 
 public class BadgesComposite extends Composite {
 
-	public BadgesComposite(Composite parent, int style) {
+	public BadgesComposite(Composite parent, int style, ResourceManager resourceManager) {
 		super(parent, style | SWT.H_SCROLL);
 		setLayout(new FillLayout(SWT.HORIZONTAL));
 
-		setBadges(null);
+		for (final IBadge badge : CharacterView.getUser().getBadges()) {
+			final Label badgeLabel = new Label(this, SWT.NONE);
+			badgeLabel.setImage(resourceManager.createImage(badge.getImageDescriptor()));
+		}
 	}
-
-	public void setBadges(Collection<IBadge> badges) {
-		// discard old content
-		for (final Control child : getChildren())
-			child.dispose();
-
-		// populate new content
-		final Label lblNewLabel_4 = new Label(this, SWT.NONE);
-		lblNewLabel_4.setImage(SWTResourceManager.getImage("/home/christian/badge2.png"));
-
-		final Label lblNewLabel_3 = new Label(this, SWT.NONE);
-		lblNewLabel_3.setImage(SWTResourceManager.getImage("/home/christian/badge1.png"));
-
-		final Label lblNewLabel_5 = new Label(this, SWT.NONE);
-		lblNewLabel_5.setImage(SWTResourceManager.getImage("/home/christian/badge3.png"));
-	}
-
 }
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterComposite.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterComposite.java
new file mode 100644
index 0000000..80aad82
--- /dev/null
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterComposite.java
@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.skills.ui.views.character;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.resource.ResourceManager;
+import org.eclipse.skills.Activator;
+import org.eclipse.skills.Logger;
+import org.eclipse.skills.service.AvatarCreator;
+import org.eclipse.skills.service.ISkillService;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.PlatformUI;
+
+public class CharacterComposite extends Composite {
+
+	public CharacterComposite(Composite parent, int style, ResourceManager resourceManager) {
+		super(parent, style);
+
+		setLayout(new GridLayout(2, false));
+
+		final Label lblCharacterImage = createImageLabel(resourceManager);
+		createLoadImageLink(lblCharacterImage, resourceManager);
+		createRandomImageLink(lblCharacterImage, resourceManager);
+	}
+
+	private Label createImageLabel(ResourceManager resourceManager) {
+		final Label avatarLabel = new Label(this, SWT.NONE);
+		avatarLabel.setLayoutData(GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.CENTER).grab(false, true).span(2, 1).indent(0, 20).create());
+
+		updateAvatarImage(avatarLabel, resourceManager);
+
+		return avatarLabel;
+	}
+
+	private void createRandomImageLink(Label lblCharacterImage, ResourceManager resourceManager) {
+		final Link lnkRandomImage = new Link(this, SWT.NONE);
+		lnkRandomImage.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+		lnkRandomImage.setText("<a>Random image</a>");
+		lnkRandomImage.addMouseListener(new MouseAdapter() {
+			@Override
+			public void mouseDown(MouseEvent e) {
+
+				try {
+					storeAvatarImage(AvatarCreator.getRandomAvatarData());
+
+					updateAvatarImage(lblCharacterImage, resourceManager);
+
+				} catch (final IOException ex) {
+					Logger.error(Activator.PLUGIN_ID, "Could not load random avatar image", ex);
+				}
+			}
+
+		});
+	}
+
+	private void createLoadImageLink(Label lblCharacterImage, ResourceManager resourceManager) {
+		final Link lnkLoadImage = new Link(this, SWT.NONE);
+		lnkLoadImage.setText("<a>Load image...</a>");
+		lnkLoadImage.addMouseListener(new MouseAdapter() {
+			@Override
+			public void mouseDown(MouseEvent e) {
+				final FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell());
+				final String location = fileDialog.open();
+				if (location != null) {
+					try {
+						final File imageFile = new File(location);
+						final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
+						skillService.storeResource(ISkillService.RESOURCE_AVATAR, Files.readAllBytes(imageFile.toPath()));
+
+						updateAvatarImage(lblCharacterImage, resourceManager);
+
+					} catch (final IOException ex) {
+						Logger.error(Activator.PLUGIN_ID, "Could not load avatar image from file: " + location, ex);
+					}
+				}
+			}
+
+		});
+	}
+
+	private void storeAvatarImage(final byte[] imageData) throws IOException {
+		final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
+		skillService.storeResource(ISkillService.RESOURCE_AVATAR, imageData);
+	}
+
+	private void updateAvatarImage(Label avatarLabel, ResourceManager resourceManager) {
+		avatarLabel.setImage(getAvatarImage(resourceManager));
+		avatarLabel.getParent().layout(true);
+	}
+
+	private Image getAvatarImage(ResourceManager resourceManager) {
+		final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
+		if (skillService != null)
+			return resourceManager.createImage(skillService.getUser().getAvatar());
+
+		return null;
+	}
+}
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterSheet.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterSheet.java
deleted file mode 100644
index 4f34caf..0000000
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterSheet.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2020 Christian Pontesegger and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v2.0
- * which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Contributors:
- *     Christian Pontesegger - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.skills.ui.views.character;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import org.eclipse.jface.resource.ImageDescriptor;
-import org.eclipse.skills.Activator;
-import org.eclipse.skills.Logger;
-import org.eclipse.skills.model.ISkill;
-import org.eclipse.skills.model.ISkillsFactory;
-import org.eclipse.skills.model.IUser;
-import org.eclipse.skills.service.AvatarCreator;
-import org.eclipse.skills.service.ISkillService;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.events.MouseAdapter;
-import org.eclipse.swt.events.MouseEvent;
-import org.eclipse.swt.graphics.Image;
-import org.eclipse.swt.graphics.ImageData;
-import org.eclipse.swt.layout.FillLayout;
-import org.eclipse.swt.layout.GridData;
-import org.eclipse.swt.layout.GridLayout;
-import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.FileDialog;
-import org.eclipse.swt.widgets.Label;
-import org.eclipse.swt.widgets.Link;
-import org.eclipse.swt.widgets.ProgressBar;
-import org.eclipse.ui.PlatformUI;
-import org.eclipse.ui.part.ViewPart;
-import org.eclipse.wb.swt.SWTResourceManager;
-
-public class CharacterSheet extends ViewPart {
-
-	/**
-	 * Creates fake user data. Needed for developers when working with the SWT designer as we do not have a real service running there.
-	 *
-	 * @return fake user instance
-	 */
-	public static IUser createDummyUser() {
-		final IUser user = ISkillsFactory.eINSTANCE.createUser();
-
-		final ISkill xp = ISkillsFactory.eINSTANCE.createSkill();
-		xp.setName("XP");
-		xp.setExperience(12345);
-		xp.setProgression(ISkillsFactory.eINSTANCE.createFactorProgression());
-
-		user.setExperience(xp);
-
-		final ISkill flexibility = ISkillsFactory.eINSTANCE.createSkill();
-		flexibility.setName("Flexibility");
-		flexibility.setExperience(445);
-		flexibility.setProgression(ISkillsFactory.eINSTANCE.createFactorProgression());
-		user.getSkills().add(flexibility);
-
-		final ISkill knowledge = ISkillsFactory.eINSTANCE.createSkill();
-		knowledge.setName("Knowledge");
-		knowledge.setExperience(5774);
-		knowledge.setProgression(ISkillsFactory.eINSTANCE.createFactorProgression());
-		user.getSkills().add(knowledge);
-
-		final ISkill scripting = ISkillsFactory.eINSTANCE.createSkill();
-		scripting.setName("Scripting");
-		scripting.setExperience(1111);
-		scripting.setProgression(ISkillsFactory.eINSTANCE.createFactorProgression());
-		user.getSkills().add(scripting);
-
-		user.setName("Hendrik");
-		user.setImageLocation("platform:/plugin/" + Activator.PLUGIN_ID + "/resources/defaultAvatars/gentleman.png");
-
-		return user;
-	}
-
-	private Label flblCharacterImage;
-	private SkillsComposite fskillsComposite;
-	private Composite fDataComposite;
-	private Label fLblTitle;
-
-	public CharacterSheet() {
-	}
-
-	@Override
-	public void createPartControl(Composite parent) {
-
-		final Composite composite = new Composite(parent, SWT.NONE);
-		composite.setLayout(new GridLayout(3, false));
-
-		fLblTitle = new Label(composite, SWT.NONE);
-		fLblTitle.setFont(SWTResourceManager.getFont("Cantarell", 24, SWT.BOLD));
-		fLblTitle.setForeground(SWTResourceManager.getColor(SWT.COLOR_TITLE_BACKGROUND));
-		final GridData gd_lblNewLabel_2 = new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1);
-		gd_lblNewLabel_2.horizontalIndent = 20;
-		fLblTitle.setLayoutData(gd_lblNewLabel_2);
-		fLblTitle.setText("Scripting Expert");
-
-		flblCharacterImage = new Label(composite, SWT.NONE);
-		flblCharacterImage.setImage(getAvatarImage());
-		final GridData gd_lblCharacterImage = new GridData(SWT.LEFT, SWT.TOP, false, true, 2, 1);
-		gd_lblCharacterImage.verticalIndent = 20;
-		flblCharacterImage.setLayoutData(gd_lblCharacterImage);
-
-		fDataComposite = new Composite(composite, SWT.NONE);
-		fDataComposite.setLayout(new GridLayout(3, false));
-		fDataComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
-		new Label(fDataComposite, SWT.NONE);
-		new Label(fDataComposite, SWT.NONE);
-		new Label(fDataComposite, SWT.NONE);
-
-		createSkillComponent(getUser().getExperience());
-
-		final Label label_1 = new Label(fDataComposite, SWT.SEPARATOR | SWT.HORIZONTAL);
-		final GridData gd_label_1 = new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1);
-		gd_label_1.verticalIndent = 20;
-		label_1.setLayoutData(gd_label_1);
-
-		final Label lblNewLabel_6 = new Label(fDataComposite, SWT.NONE);
-		lblNewLabel_6.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
-		lblNewLabel_6.setText("Your Skills:");
-		new Label(fDataComposite, SWT.NONE);
-		new Label(fDataComposite, SWT.NONE);
-
-		fskillsComposite = new SkillsComposite(fDataComposite, SWT.NONE);
-		fskillsComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
-		new Label(fDataComposite, SWT.NONE);
-
-		final BadgesComposite badgesComposite = new BadgesComposite(fDataComposite, SWT.H_SCROLL);
-		final FillLayout fillLayout = (FillLayout) badgesComposite.getLayout();
-		fillLayout.spacing = 20;
-		badgesComposite.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1));
-		new Label(fDataComposite, SWT.NONE);
-
-		final Label label = new Label(fDataComposite, SWT.SEPARATOR | SWT.HORIZONTAL);
-		label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
-		new Label(fDataComposite, SWT.NONE);
-
-		final Link link = new Link(composite, SWT.NONE);
-		link.addMouseListener(new MouseAdapter() {
-			@Override
-			public void mouseDown(MouseEvent e) {
-				final FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell());
-				final String location = fileDialog.open();
-				if (location != null) {
-					try {
-						final File imageFile = new File(location);
-						final Image image = ImageDescriptor.createFromURL(imageFile.toURL()).createImage();
-						// FIXME resource handling
-						flblCharacterImage.setImage(image);
-						flblCharacterImage.getParent().layout(true);
-
-						final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
-						skillService.storeResource(ISkillService.RESOURCE_AVATAR, Files.readAllBytes(imageFile.toPath()));
-
-					} catch (final IOException ex) {
-						Logger.error(Activator.PLUGIN_ID, "Could not load avatar image from file: " + location, ex);
-					}
-				}
-			}
-		});
-		link.setText("<a>Load image...</a>");
-
-		final Link link_1 = new Link(composite, SWT.NONE);
-		link_1.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
-		link_1.setText("<a>Random image</a>");
-		link_1.addMouseListener(new MouseAdapter() {
-			@Override
-			public void mouseDown(MouseEvent e) {
-
-				try {
-					final byte[] imageData = AvatarCreator.getRandomAvatarData();
-					final Image image = new Image(Display.getDefault(), new ImageData(new ByteArrayInputStream(imageData)));
-					// FIXME resource handling
-					flblCharacterImage.setImage(image);
-					flblCharacterImage.getParent().layout(true);
-
-					final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
-					skillService.storeResource(ISkillService.RESOURCE_AVATAR, imageData);
-
-				} catch (final IOException ex) {
-					Logger.error(Activator.PLUGIN_ID, "Could not load random avatar image", ex);
-				}
-			}
-		});
-		new Label(composite, SWT.NONE);
-
-		loadUserData();
-	}
-
-	private void createSkillComponent(ISkill skill) {
-		final Label lblNewLabel = new Label(fDataComposite, SWT.NONE);
-		lblNewLabel.setText(skill.getName() + ":");
-
-		final ProgressBar prgExperience = new ProgressBar(fDataComposite, SWT.SMOOTH);
-		prgExperience.setMinimum(100);
-		prgExperience.setMaximum(1000);
-		prgExperience.setSelection(100);
-		prgExperience.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
-		prgExperience.setToolTipText("2523 / 3000");
-
-		final Label lblNewLabel_1 = new Label(fDataComposite, SWT.NONE);
-		lblNewLabel_1.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
-		lblNewLabel_1.setText("Level " + skill.getProgression().getLevel(skill.getExperience()));
-	}
-
-	/**
-	 * Get the avatar image.
-	 *
-	 * @return
-	 */
-	private Image getAvatarImage() {
-		// FIXME add resource handling
-		final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
-		if (skillService != null)
-			return new Image(Display.getDefault(), skillService.getUser().getAvatar());
-
-		return SWTResourceManager.getImage("/home/christian/character.png");
-	}
-
-	@Override
-	public void setFocus() {
-	}
-
-	public IUser getUser() {
-		final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
-		if (skillService != null)
-			return skillService.getUser();
-
-		// needed when used in development mode in SWT designer
-		return createDummyUser();
-	}
-
-	public void loadUserData() {
-		final IUser user = getUser();
-		final String imageLocation = user.getImageLocation();
-		if (imageLocation != null) {
-			// FIXME add resource handling
-			try {
-				final ImageDescriptor imageDescriptor = ImageDescriptor.createFromURL(new URL(imageLocation));
-				flblCharacterImage.setImage(imageDescriptor.createImage());
-
-			} catch (final MalformedURLException e) {
-				Logger.error(Activator.PLUGIN_ID, "Cannot load character image from \"" + imageLocation + "\"", e);
-			}
-		}
-
-		fLblTitle.setText(user.getName() + ", an expert (TODO)");
-
-		for (final ISkill skill : getSkillsSortedByProgress(user))
-			createSkillComponent(skill);
-
-		fskillsComposite.setSkills(user.getSkills());
-	}
-
-	private List<ISkill> getSkillsSortedByProgress(final IUser user) {
-		final List<ISkill> skills = new ArrayList<>(user.getSkills());
-		Collections.sort(skills, (o1, o2) -> {
-			if (o1.getProgression().getLevel(o1.getExperience()) == o2.getProgression().getLevel(o2.getExperience()))
-				return o2.getExperience() - o1.getExperience();
-
-			return o2.getProgression().getLevel(o2.getExperience()) - o1.getProgression().getLevel(o1.getExperience());
-		});
-
-		return skills;
-	}
-}
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterView.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterView.java
new file mode 100644
index 0000000..6634627
--- /dev/null
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/CharacterView.java
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.skills.ui.views.character;
+
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.skills.model.IUser;
+import org.eclipse.skills.service.ISkillService;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.part.ViewPart;
+import org.eclipse.wb.swt.SWTResourceManager;
+
+public class CharacterView extends ViewPart {
+
+	public static IUser getUser() {
+		final ISkillService skillService = PlatformUI.getWorkbench().getService(ISkillService.class);
+		if (skillService != null)
+			return skillService.getUser();
+
+		return null;
+	}
+
+	private StatsComposite fStatsComposite;
+	private LocalResourceManager fResourceManager;
+
+	public CharacterView() {
+	}
+
+	@Override
+	public void createPartControl(Composite parent) {
+
+		final Composite composite = new Composite(parent, SWT.NONE);
+		composite.setLayout(new GridLayout(2, false));
+
+		fResourceManager = new LocalResourceManager(JFaceResources.getResources(), composite);
+
+		final Label lblTitle = addTitle(composite);
+
+		final CharacterComposite characterComposite = new CharacterComposite(composite, SWT.NONE, fResourceManager);
+		characterComposite.setLayoutData(GridDataFactory.fillDefaults().grab(false, true).create());
+
+		fStatsComposite = new StatsComposite(composite, SWT.NONE, fResourceManager);
+		fStatsComposite.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
+	}
+
+	private Label addTitle(final Composite parent) {
+		// FIXME use default font for headers
+		final Label lblTitle = new Label(parent, SWT.NONE);
+		lblTitle.setFont(SWTResourceManager.getFont("Cantarell", 24, SWT.BOLD));
+		lblTitle.setForeground(SWTResourceManager.getColor(SWT.COLOR_TITLE_BACKGROUND));
+		lblTitle.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).span(2, 1).indent(20, 0).create());
+		lblTitle.setText(getUser().getName() + ", an expert (TODO)");
+
+		return lblTitle;
+	}
+
+	@Override
+	public void setFocus() {
+	}
+}
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/SkillsComposite.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/SkillsComposite.java
index 9b84fec..f19afb7 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/SkillsComposite.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/SkillsComposite.java
@@ -13,49 +13,126 @@
 
 package org.eclipse.skills.ui.views.character;
 
+import java.time.Duration;
 import java.util.Collection;
+import java.util.List;
 
+import org.eclipse.emf.common.notify.Adapter;
+import org.eclipse.emf.common.notify.Notification;
+import org.eclipse.emf.common.notify.impl.AdapterImpl;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.util.Throttler;
 import org.eclipse.skills.model.ISkill;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.ProgressBar;
 
 public class SkillsComposite extends Composite {
 
-	public SkillsComposite(Composite parent, int style) {
+	private static final String FLAG_WITH_PROGRESSBAR = "hasProgressbar";
+
+	private final boolean fShowProgress;
+
+	private final Throttler fUiUpdateThrottler = new Throttler(Display.getDefault(), Duration.ofMillis(500), this::updateControls);
+
+	private final Adapter fSkillAdapter = new SkillAdapter();
+
+	public SkillsComposite(Composite parent, int style, List<ISkill> skills, boolean showProgress) {
 		super(parent, style);
 
-		setLayout(new GridLayout(3, false));
+		fShowProgress = showProgress;
+
+		setLayout(new GridLayout(showProgress ? 3 : 2, false));
+
+		setSkills(skills);
 	}
 
 	public void setSkills(Collection<ISkill> skills) {
-		// discard old content
-		for (final Control child : getChildren())
-			child.dispose();
 
-		// populate new content
 		for (final ISkill skill : skills) {
-			final Label lblTitle = new Label(this, SWT.NONE);
-			lblTitle.setText(skill.getName());
-			// FIXME needs better getter for plain text/ abstract
-			lblTitle.setToolTipText(skill.getDescription().toString());
-
-			final ProgressBar progressBar = new ProgressBar(this, SWT.NONE);
-			progressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
-			progressBar.setMinimum(0);
-			progressBar.setMaximum(100);
-			progressBar.setSelection(skill.getExperience());
-
-			final Label lblLevel = new Label(this, SWT.NONE);
-			// FIXME get correct level
-			lblLevel.setText("Lvl 7");
+			createSkillElement(this, skill);
+			skill.eAdapters().add(fSkillAdapter);
 		}
 
 		// trigger fresh layout
 		getParent().layout();
 	}
+
+	private void createSkillElement(Composite parent, ISkill skill) {
+		final Label lblTitle = new Label(parent, SWT.NONE);
+		lblTitle.setText(skill.getName());
+		lblTitle.setToolTipText(skill.getDescription().getText());
+		lblTitle.setLayoutData(GridDataFactory.fillDefaults().create());
+
+		if (fShowProgress) {
+			final ProgressBar progressBar = new ProgressBar(parent, SWT.NONE);
+			progressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+			progressBar.setData(skill);
+			updateControl(progressBar, skill);
+
+			final Label lblLevel = new Label(parent, SWT.NONE);
+			lblLevel.setData(skill);
+			lblLevel.setData(FLAG_WITH_PROGRESSBAR, true);
+			updateControl(lblLevel, skill);
+
+		} else {
+
+			final Label lblLevel = new Label(parent, SWT.NONE);
+			lblLevel.setData(skill);
+			lblLevel.setData(FLAG_WITH_PROGRESSBAR, false);
+			lblLevel.setLayoutData(GridDataFactory.fillDefaults().indent(20, 0).create());
+			updateControl(lblLevel, skill);
+		}
+	}
+
+	public void updateControls() {
+		for (final Control control : getChildren()) {
+			if (control.getData() instanceof ISkill)
+				updateControl(control, (ISkill) control.getData());
+		}
+	}
+
+	public void updateControl(Control control, ISkill skill) {
+		final int level = skill.getProgression().getLevel(skill.getExperience());
+		final int currentLevelMinXP = skill.getProgression().getMinimumXpForLevel(level);
+		final int nextLevelMinXP = skill.getProgression().getMinimumXpForLevel(level + 1);
+
+		if (control instanceof Label) {
+			if (hasProgressbar(control)) {
+				((Label) control).setText(String.format("Level %d", level));
+				((Label) control).setToolTipText(
+						String.format("Earn %d more %s to reach level %d", (nextLevelMinXP - skill.getExperience()), skill.getName(), level + 1));
+
+			} else {
+				((Label) control).setText(String.format("%d", level));
+				((Label) control).setToolTipText(
+						String.format("Earn %d more %s points to reach level %d", (nextLevelMinXP - skill.getExperience()), skill.getName(), level + 1));
+			}
+
+		} else if (control instanceof ProgressBar) {
+			((ProgressBar) control).setMinimum(currentLevelMinXP);
+			((ProgressBar) control).setMaximum(nextLevelMinXP);
+			((ProgressBar) control).setSelection(skill.getExperience());
+			((ProgressBar) control).setToolTipText(String.format("%d %s XP", skill.getExperience(), skill.getName()));
+		}
+	}
+
+	private boolean hasProgressbar(Control control) {
+		final Object hasProgressbar = control.getData(FLAG_WITH_PROGRESSBAR);
+		return (hasProgressbar instanceof Boolean) ? (Boolean) hasProgressbar : true;
+	}
+
+	private class SkillAdapter extends AdapterImpl {
+		@Override
+		public void notifyChanged(Notification msg) {
+
+			if (msg.getNotifier() instanceof ISkill)
+				fUiUpdateThrottler.throttledExec();
+		}
+	}
 }
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/StatsComposite.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/StatsComposite.java
new file mode 100644
index 0000000..66fdb0e
--- /dev/null
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/character/StatsComposite.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.skills.ui.views.character;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.emf.common.util.EList;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.resource.ResourceManager;
+import org.eclipse.skills.model.ISkill;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+
+public class StatsComposite extends Composite {
+
+	private static final int INDENTATION = 30;
+
+	private static int compareSkillsByName(ISkill arg0, ISkill arg1) {
+		return arg0.getName().compareTo(arg1.getName());
+	}
+
+	private static List<ISkill> getPrimarySkills(Collection<ISkill> skills) {
+		final List<ISkill> primarySkills = skills.stream().filter(s -> s.isBaseSkill()).collect(Collectors.toList());
+		Collections.sort(primarySkills, StatsComposite::compareSkillsByName);
+
+		return primarySkills;
+	}
+
+	private static List<ISkill> getSecondarySkills(Collection<ISkill> skills) {
+		final List<ISkill> secondarySkills = skills.stream().filter(s -> !s.isBaseSkill()).collect(Collectors.toList());
+		Collections.sort(secondarySkills, StatsComposite::compareSkillsByName);
+
+		return secondarySkills;
+	}
+
+	public StatsComposite(Composite parent, int style, ResourceManager resourceManager) {
+		super(parent, style);
+
+		setLayout(new GridLayout());
+
+		final SkillsComposite xp = new SkillsComposite(this, SWT.NONE, Arrays.asList(CharacterView.getUser().getExperience()), true);
+		xp.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.FILL).create());
+
+		addSeparator();
+
+		final Label lblPrimarySkills = new Label(this, SWT.NONE);
+		lblPrimarySkills.setText("Your Stats:");
+
+		final List<ISkill> primarySkills = getPrimarySkills(CharacterView.getUser().getSkills());
+		final SkillsComposite primary = new SkillsComposite(this, SWT.NONE, primarySkills, false);
+		primary.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).indent(INDENTATION, 0).create());
+
+		addSeparator();
+
+		final Label lblSecondarySkills = new Label(this, SWT.NONE);
+		lblSecondarySkills.setText("Your Skills:");
+
+		final List<ISkill> secondarySkills = getSecondarySkills(CharacterView.getUser().getSkills());
+		final SkillsComposite secondary = new SkillsComposite(this, SWT.NONE, secondarySkills, true);
+		secondary.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).indent(INDENTATION, 0).create());
+
+		final BadgesComposite badgesComposite = new BadgesComposite(this, SWT.H_SCROLL, resourceManager);
+		badgesComposite.setLayoutData(GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.BOTTOM).grab(true, true).create());
+	}
+
+	private void addSeparator() {
+		final Label separator = new Label(this, SWT.SEPARATOR | SWT.HORIZONTAL);
+		separator.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.FILL).create());
+	}
+
+	public void setSkills(EList<ISkill> skills) {
+		// TODO Auto-generated method stub
+
+	}
+
+}
diff --git a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/journal/JournalView.java b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/journal/JournalView.java
index abde37d..3ba8243 100644
--- a/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/journal/JournalView.java
+++ b/plugins/org.eclipse.skills/src/org/eclipse/skills/ui/views/journal/JournalView.java
@@ -31,7 +31,6 @@
 import org.eclipse.skills.model.IUser;
 import org.eclipse.skills.model.IUserTask;
 import org.eclipse.skills.service.ISkillService;
-import org.eclipse.skills.ui.views.character.CharacterSheet;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.SashForm;
 import org.eclipse.swt.graphics.Image;
@@ -156,13 +155,6 @@
 		return composite;
 	}
 
-	private Composite createDetailHeaderComposite(Composite parent, FormToolkit toolkit) {
-		final Composite composite = toolkit.createComposite(parent);
-		composite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 1, 1));
-
-		return composite;
-	}
-
 	private java.util.List<IUserTask> getOpenTasks() {
 		final java.util.List<IUserTask> tasks = getUser().getUsertasks().stream().filter(t -> !t.isStarted()).collect(Collectors.toList());
 
@@ -195,8 +187,7 @@
 		if (skillService != null)
 			return skillService.getUser();
 
-		// needed when used in development mode in SWT designer
-		return CharacterSheet.createDummyUser();
+		return null;
 	}
 
 	@Override
diff --git a/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/DataStorageProxyTest.java b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/DataStorageProxyTest.java
deleted file mode 100644
index 5cb4a7c..0000000
--- a/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/DataStorageProxyTest.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2020 Christian Pontesegger and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v2.0
- * which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Contributors:
- *     Christian Pontesegger - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.skills.service;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.EOFException;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-public class DataStorageProxyTest {
-
-	private static byte[] DEFAULT_DATA = "Default data".getBytes();
-
-	private IDataStorage fBaseStorage;
-	private DataStorageProxy fDataStorage;
-
-	@BeforeEach
-	public void setup() {
-		fBaseStorage = mock(IDataStorage.class);
-		fDataStorage = new DataStorageProxy(fBaseStorage);
-	}
-
-	@Test
-	@DisplayName("constructor throws when baseStorage is null")
-	public void throwsWhenBaseStorageIsNull() {
-		assertThrows(IllegalArgumentException.class, () -> new DataStorageProxy(null));
-	}
-
-	@Test
-	@DisplayName("getStorage() returns baseStorage")
-	public void getStorage() {
-		assertEquals(fBaseStorage, fDataStorage.getStorage());
-	}
-
-	@Test
-	@DisplayName("hasResource() == true when resource exists")
-	public void hasResourceIsTrueWhenResourceExists() {
-		when(fBaseStorage.hasResource(any())).thenReturn(true);
-
-		assertTrue(fDataStorage.hasResource("any"));
-	}
-
-	@Test
-	@DisplayName("hasResource() == false when resource does not exist")
-	public void hasResourceIsFalseWhenResourceDoesNotExist() {
-		when(fBaseStorage.hasResource(any())).thenReturn(false);
-
-		assertFalse(fDataStorage.hasResource("any"));
-	}
-
-	@Test
-	@DisplayName("loadResource() throws when resource does not exist")
-	public void loadResourceThrowsWhenResourceDoesNotExist() {
-		when(fBaseStorage.hasResource(any())).thenReturn(false);
-
-		assertThrows(FileNotFoundException.class, () -> fDataStorage.loadResource("any"));
-	}
-
-	@Test
-	@DisplayName("loadResource() returns data when resource exists")
-	public void loadResourceReturnsDataWhenResourceExists() throws IOException {
-		when(fBaseStorage.hasResource(any())).thenReturn(true);
-		when(fBaseStorage.loadResource(any())).thenReturn(DEFAULT_DATA);
-
-		assertArrayEquals(DEFAULT_DATA, fDataStorage.loadResource("any"));
-	}
-
-	@Test
-	@DisplayName("loadResource() throws when base storage throws")
-	public void loadResourceThrowsWhenBaseStorageThrows() throws IOException {
-		when(fBaseStorage.hasResource(any())).thenReturn(true);
-		when(fBaseStorage.loadResource(any())).thenThrow(new EOFException());
-
-		assertThrows(EOFException.class, () -> fDataStorage.loadResource("any"));
-	}
-
-	@Test
-	@DisplayName("hasResource() == true after storeResource()")
-	public void hasResourceIsTrueAfterStoreResource() throws IOException {
-		when(fBaseStorage.hasResource(any())).thenReturn(false).thenReturn(true);
-
-		assertFalse(fDataStorage.hasResource("any"));
-		fDataStorage.storeResource("any", DEFAULT_DATA);
-		assertTrue(fDataStorage.hasResource("any"));
-
-		verify(fBaseStorage, times(1)).storeResource(any(), any());
-	}
-
-	@Test
-	@DisplayName("loadResource() returns storeResource() data")
-	public void loadResourceReturnsStoreResourceData() throws IOException {
-		when(fBaseStorage.hasResource(any())).thenReturn(true);
-		when(fBaseStorage.loadResource(any())).thenReturn(DEFAULT_DATA);
-
-		fDataStorage.storeResource("any", DEFAULT_DATA);
-		assertArrayEquals(DEFAULT_DATA, fDataStorage.loadResource("any"));
-
-		verify(fBaseStorage, times(1)).storeResource(any(), any());
-	}
-
-	@Test
-	@DisplayName("storeResource() throws when base storage throws")
-	public void storeResourceThrowsWhenBaseStorageThrows() throws IOException {
-		doThrow(new EOFException()).when(fBaseStorage).storeResource(any(), any());
-
-		assertThrows(EOFException.class, () -> fDataStorage.storeResource("any", DEFAULT_DATA));
-	}
-
-	@Test
-	@DisplayName("setStorage() throws when baseStorage == null")
-	public void setStorageThrowsOnNullParameter() {
-		assertThrows(IllegalArgumentException.class, () -> fDataStorage.setStorage(null));
-	}
-
-	@Test
-	@DisplayName("setStorage() allows to replace storage")
-	public void setStorageSetsTheStorage() {
-		when(fBaseStorage.hasResource(any())).thenReturn(true);
-
-		final IDataStorage newStorage = mock(IDataStorage.class);
-		when(newStorage.hasResource(any())).thenReturn(false);
-
-		assertTrue(fDataStorage.hasResource("any"));
-
-		fDataStorage.setStorage(newStorage);
-		assertEquals(newStorage, fDataStorage.getStorage());
-		assertFalse(fDataStorage.hasResource("any"));
-	}
-}
diff --git a/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/UserStorageTest.java b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/UserStorageTest.java
index 629289a..2ea8c61 100644
--- a/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/UserStorageTest.java
+++ b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/UserStorageTest.java
@@ -30,6 +30,7 @@
 
 import org.eclipse.skills.model.IUser;
 import org.eclipse.skills.model.IUserTask;
+import org.eclipse.skills.service.storage.IDataStorage;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
diff --git a/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/AbstractStorageTest.java b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/AbstractStorageTest.java
new file mode 100644
index 0000000..6e9b7ee
--- /dev/null
+++ b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/AbstractStorageTest.java
@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.skills.service.storage;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public abstract class AbstractStorageTest {
+
+	private static final String UNKNOWN_FILE = "doesNotExist";
+	private static final String DEFAULT_FILE = "defaultFile";
+	public static final byte[] DEFAULT_CONTENT = "Lorem ipsum dolorem".getBytes();
+
+	private static byte[] readInputStream(InputStream input) throws IOException {
+		final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+		final byte[] buffer = new byte[1024];
+		int bytesRead = input.read(buffer);
+		while (bytesRead > 0) {
+			out.write(buffer, 0, bytesRead);
+			bytesRead = input.read(buffer);
+		}
+
+		return out.toByteArray();
+	}
+
+	private IDataStorage fDataStorage;
+
+	@BeforeEach
+	public void setup() {
+		fDataStorage = createStorage();
+	}
+
+	public IDataStorage getDataStorage() {
+		return fDataStorage;
+	}
+
+	/**
+	 * Create an instance of the storage under test.
+	 *
+	 * @return storage under test
+	 */
+	protected abstract IDataStorage createStorage();
+
+	@Test
+	@DisplayName("hasResource() == false when file does not exist")
+	public void hasResourceFalseWhenFileDoesNotExist() {
+		assertFalse(fDataStorage.hasResource(UNKNOWN_FILE));
+	}
+
+	@Test
+	@DisplayName("hasResource() == true when file exists")
+	public void hasResourceTrueWhenFileExists() throws IOException {
+
+		fDataStorage.storeResource(DEFAULT_FILE, DEFAULT_CONTENT);
+
+		assertTrue(fDataStorage.hasResource(DEFAULT_FILE));
+	}
+
+	@Test
+	@DisplayName("loadResource() throws when file does not exist")
+	public void loadResourceThrowsWhenFileDoesNotExist() {
+		assertThrows(IOException.class, () -> fDataStorage.loadResource(UNKNOWN_FILE));
+	}
+
+	@Test
+	@DisplayName("loadResource() returns file content when file exists")
+	public void loadResourceReturnsFileContent() throws IOException {
+		fDataStorage.storeResource(DEFAULT_FILE, DEFAULT_CONTENT);
+
+		assertArrayEquals(DEFAULT_CONTENT, fDataStorage.loadResource(DEFAULT_FILE));
+	}
+
+	@Test
+	@DisplayName("openResource() throws when file does not exist")
+	public void openResourceThrowsWhenFileDoesNotExist() {
+		assertThrows(IOException.class, () -> fDataStorage.openResource(UNKNOWN_FILE));
+	}
+
+	@Test
+	@DisplayName("openResource() returns file content when file exists")
+	public void openResourceReturnsFileContent() throws IOException {
+		fDataStorage.storeResource(DEFAULT_FILE, DEFAULT_CONTENT);
+
+		assertArrayEquals(DEFAULT_CONTENT, readInputStream(fDataStorage.openResource(DEFAULT_FILE)));
+	}
+
+	@Test
+	@DisplayName("stored resource can be reloaded")
+	public void storedResourceCanBeReloaded() throws IOException {
+		final String content = new String(DEFAULT_CONTENT) + System.currentTimeMillis();
+		fDataStorage.storeResource(DEFAULT_FILE, content.getBytes());
+
+		assertArrayEquals(content.getBytes(), fDataStorage.loadResource(DEFAULT_FILE));
+	}
+
+	@Test
+	@DisplayName("hasResource() == true after storeResource()")
+	public void hasResourceIsTrueAfterStoreResource() throws IOException {
+		final String fileName = Long.toString(System.currentTimeMillis());
+
+		assertFalse(getDataStorage().hasResource(fileName));
+		getDataStorage().storeResource(fileName, DEFAULT_CONTENT);
+		assertTrue(getDataStorage().hasResource(fileName));
+	}
+}
diff --git a/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/DataStorageProxyTest.java b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/DataStorageProxyTest.java
new file mode 100644
index 0000000..d042adc
--- /dev/null
+++ b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/DataStorageProxyTest.java
@@ -0,0 +1,188 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.skills.service.storage;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class DataStorageProxyTest extends AbstractStorageTest {
+
+	private IDataStorage fBaseStorage;
+
+	@Override
+	protected IDataStorage createStorage() {
+		fBaseStorage = mock(IDataStorage.class);
+		return new DataStorageProxy(fBaseStorage);
+	}
+
+	@Override
+	public DataStorageProxy getDataStorage() {
+		return (DataStorageProxy) super.getDataStorage();
+	}
+
+	@Test
+	@DisplayName("constructor throws when baseStorage is null")
+	public void throwsWhenBaseStorageIsNull() {
+		assertThrows(IllegalArgumentException.class, () -> new DataStorageProxy(null));
+	}
+
+	@Test
+	@DisplayName("getStorage() returns baseStorage")
+	public void getStorage() {
+		assertEquals(fBaseStorage, getDataStorage().getStorage());
+	}
+
+	@Override
+	@Test
+	@DisplayName("hasResource() == false when file does not exist")
+	public void hasResourceFalseWhenFileDoesNotExist() {
+		when(fBaseStorage.hasResource(any())).thenReturn(false);
+
+		super.hasResourceFalseWhenFileDoesNotExist();
+	}
+
+	@Override
+	@Test
+	@DisplayName("hasResource() == true when file exists")
+	public void hasResourceTrueWhenFileExists() throws IOException {
+		when(fBaseStorage.hasResource(any())).thenReturn(true);
+
+		super.hasResourceTrueWhenFileExists();
+	}
+
+	@Override
+	@Test
+	@DisplayName("loadResource() throws when file does not exist")
+	public void loadResourceThrowsWhenFileDoesNotExist() {
+		when(fBaseStorage.hasResource(any())).thenReturn(false);
+
+		super.loadResourceThrowsWhenFileDoesNotExist();
+	}
+
+	@Override
+	@Test
+	@DisplayName("loadResource() returns file content when file exists")
+	public void loadResourceReturnsFileContent() throws IOException {
+		when(fBaseStorage.hasResource(any())).thenReturn(true);
+		when(fBaseStorage.loadResource(any())).thenReturn(DEFAULT_CONTENT);
+
+		super.loadResourceReturnsFileContent();
+	}
+
+	@Test
+	@DisplayName("loadResource() throws when base storage throws")
+	public void loadResourceThrowsWhenBaseStorageThrows() throws IOException {
+		when(fBaseStorage.hasResource(any())).thenReturn(true);
+		when(fBaseStorage.loadResource(any())).thenThrow(new EOFException());
+
+		assertThrows(EOFException.class, () -> getDataStorage().loadResource("any"));
+	}
+
+	@Override
+	@Test
+	@DisplayName("openResource() throws when file does not exist")
+	public void openResourceThrowsWhenFileDoesNotExist() {
+		when(fBaseStorage.hasResource(any())).thenReturn(false);
+
+		super.openResourceThrowsWhenFileDoesNotExist();
+	}
+
+	@Override
+	@Test
+	@DisplayName("openResource() returns file content when file exists")
+	public void openResourceReturnsFileContent() throws IOException {
+		when(fBaseStorage.hasResource(any())).thenReturn(true);
+		when(fBaseStorage.openResource(any())).thenReturn(new ByteArrayInputStream(DEFAULT_CONTENT));
+
+		super.openResourceReturnsFileContent();
+	}
+
+	@Test
+	@DisplayName("openResource() throws when base storage throws")
+	public void openResourceThrowsWhenBaseStorageThrows() throws IOException {
+		when(fBaseStorage.hasResource(any())).thenReturn(true);
+		when(fBaseStorage.openResource(any())).thenThrow(new EOFException());
+
+		assertThrows(EOFException.class, () -> getDataStorage().openResource("any"));
+	}
+
+	@Override
+	@Test
+	@DisplayName("stored resource can be reloaded")
+	public void storedResourceCanBeReloaded() throws IOException {
+		when(fBaseStorage.hasResource(any())).thenReturn(true);
+		when(fBaseStorage.loadResource(any())).thenReturn(DEFAULT_CONTENT);
+
+		getDataStorage().storeResource("any", DEFAULT_CONTENT);
+		assertArrayEquals(DEFAULT_CONTENT, getDataStorage().loadResource("any"));
+
+		verify(fBaseStorage, times(1)).storeResource(any(), any());
+	}
+
+	@Override
+	@Test
+	@DisplayName("hasResource() == true after storeResource()")
+	public void hasResourceIsTrueAfterStoreResource() throws IOException {
+		when(fBaseStorage.hasResource(any())).thenReturn(false).thenReturn(true);
+
+		super.hasResourceIsTrueAfterStoreResource();
+
+		verify(fBaseStorage, times(1)).storeResource(any(), any());
+	}
+
+	@Test
+	@DisplayName("storeResource() throws when base storage throws")
+	public void storeResourceThrowsWhenBaseStorageThrows() throws IOException {
+		doThrow(new EOFException()).when(fBaseStorage).storeResource(any(), any());
+
+		assertThrows(EOFException.class, () -> getDataStorage().storeResource("any", DEFAULT_CONTENT));
+	}
+
+	@Test
+	@DisplayName("setStorage() throws when baseStorage == null")
+	public void setStorageThrowsOnNullParameter() {
+		assertThrows(IllegalArgumentException.class, () -> getDataStorage().setStorage(null));
+	}
+
+	@Test
+	@DisplayName("setStorage() allows to replace storage")
+	public void setStorageSetsTheStorage() {
+		when(fBaseStorage.hasResource(any())).thenReturn(true);
+
+		final IDataStorage newStorage = mock(IDataStorage.class);
+		when(newStorage.hasResource(any())).thenReturn(false);
+
+		assertTrue(getDataStorage().hasResource("any"));
+
+		getDataStorage().setStorage(newStorage);
+		assertEquals(newStorage, getDataStorage().getStorage());
+		assertFalse(getDataStorage().hasResource("any"));
+	}
+}
diff --git a/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/WorkspaceDataStorageTest.java b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/WorkspaceDataStorageTest.java
new file mode 100644
index 0000000..5d55ca2
--- /dev/null
+++ b/tests/org.eclipse.skills.test/src/org/eclipse/skills/service/storage/WorkspaceDataStorageTest.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.skills.service.storage;
+
+public class WorkspaceDataStorageTest extends AbstractStorageTest {
+
+	@Override
+	protected IDataStorage createStorage() {
+		return new WorkspaceDataStorage();
+	}
+
+}