<commit_before>package org.oasis.datacore.model.resource;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.oasis.datacore.core.meta.SimpleUriService;
import org.oasis.datacore.core.meta.model.DCField;
import org.oasis.datacore.core.meta.model.DCI18nField;
import org.oasis.datacore.core.meta.model.DCListField;
import org.oasis.datacore.core.meta.model.DCMapField;
import org.oasis.datacore.core.meta.model.DCModel;
import org.oasis.datacore.core.meta.model.DCModelBase;
import org.oasis.datacore.core.meta.model.DCModelService;
import org.oasis.datacore.core.meta.model.DCResourceField;
import org.oasis.datacore.core.meta.model.DCSecurity;
import org.oasis.datacore.core.meta.pov.DCProject;
import org.oasis.datacore.rest.api.DCResource;
import org.oasis.datacore.rest.api.util.DCURI;
import org.oasis.datacore.rest.api.util.UriHelper;
import org.oasis.datacore.rest.server.parsing.exception.ResourceParsingException;
import org.oasis.datacore.rest.server.parsing.model.DCResourceParsingContext;
import org.oasis.datacore.rest.server.resource.ResourceEntityMapperService;
import org.oasis.datacore.rest.server.resource.ResourceException;
import org.oasis.datacore.rest.server.resource.ValueParsingService;
import org.oasis.datacore.sample.ResourceModelIniter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;

@Component
public class ModelResourceMappingService {
   
   @Autowired
   private DCModelService dataModelService;
   @Autowired
   private ValueParsingService valueService;
   /** to parse i18n default value according to default language
    * LATER use it in the whole model mapping process ? */
   @Autowired
   private ResourceEntityMapperService resourceEntityMapperService;
   
   
   //////////////////////////////////////////////////////
   // DCModel to Resource (used in ResourceModelIniter) :

   /** public for tests */
   public String buildModelUri(DCResource modelResource) {
      return SimpleUriService.buildModelUri(
            (String) modelResource.get("dcmo:name")/* + '_' + modelResource.get("dcmo:majorVersion")*/); // LATER refactor
   }
   /** only for tests */
   public String buildModelUri(DCModelBase model) {
      return SimpleUriService.buildModelUri(
            (String) model.getName()/* + '_' + model.getMajorVersion()*/); // LATER refactor
   }
   
   /** public for tests */
   public String buildFieldUriPrefix(DCURI dcUri) {
      return SimpleUriService.buildUri(ResourceModelIniter.MODEL_FIELD_NAME,
            dcUri.getType() + '/' + dcUri.getId()).toString();
   }

   /**
    * Used by all POVs
    * @param modelType
    * @param name
    * @return
    */
   public String buildNameUri(String modelType, String name) {
      return SimpleUriService.buildUri(modelType, name);
   }
   
   /**
    * NOT USED
    * public for tests
    * @param projectResource
    * @return
    * @throws RuntimeException if missing model type, TODO better ex. ResourceParsingException
    */
   public String buildPointOfViewUri(DCResource projectResource) throws RuntimeException {
      List<String> types = projectResource.getTypes();
      if (types == null || types.isEmpty()) {
         throw new RuntimeException("Missing Model type");
      }
      return SimpleUriService.buildUri(types.get(0),
            (String) projectResource.get(ResourceModelIniter.POINTOFVIEW_NAME_PROP));
   }

   /**
    * 
    * @param project
    * @return
    * @throws ResourceParsingException
    */
   public DCResource projectToResource(DCProject project) throws ResourceParsingException {
      DCResource projectResource = DCResource.create(null, ResourceModelIniter.MODEL_PROJECT_NAME);
      return projectToResource(project, projectResource);
   }
   public DCResource projectToResource(DCProject project, DCResource projectResource) throws ResourceParsingException {
      projectResource
            .set("dcmpv:name", project.getName())
            .set("dcmpv:majorVersion", project.getMajorVersion())
            .set("dcmpv:unversionedName", project.getUnversionedName())
            // NB. NOT o:version which is only in DCProject for info purpose
         
            // NB. minor version is Resource version
            .set("dcmpv:documentation", project.getDocumentation())// TODO in another collection for performance
            
            // POLY cache : TODO
            ;
      
      String uri = this.buildPointOfViewUri(projectResource);

      //projectResource.set("dcmpv:pointOfViews", pointOfViewsToNames(project.getLocalVisibleProjects())); // TODO as cache ?!
      projectResource.set("dcmp:localVisibleProjects", project.getLocalVisibleProjects().stream()
            .map(p -> this.buildNameUri(ResourceModelIniter.MODEL_PROJECT_NAME, p.getName())).collect(Collectors.toList())); // NOT toSet else JSON'd as HashMap
      projectResource.set("dcmp:visibleProjectNames", new ArrayList<String>(
            dataModelService.getVisibleProjectNames(project.getName()))); // only to display for now
      projectResource.set("dcmp:forkedUris", new ArrayList<String>(
            project.getForkedUris())); // only to display for now
      projectResource.set("dcmp:frozenModelNames", new ArrayList<String>(
            project.getFrozenModelNames()));
      projectResource.set("dcmp:useCasePointOfViews", project.getUseCasePointOfViews().stream()
            .map(ucpov -> {
               //useCasePointOfViewToResource(ucpov); // TODO TODO & useCasePointOfViewElementToResource(ucpovelt)
               //ucpov.getPointOfViews().stream().map(ucpovelt -> mrMappingService.buildPointOfViewUri(ucpovelt)).collect(Collectors.toList())); // NOT toSet else JSON'd as HashMap
               return this.buildNameUri(ResourceModelIniter.MODEL_POINTOFVIEW_NAME, ucpov.getName());
            }).collect(Collectors.toList())); // NOT toSet else JSON'd as HashMap
      if (project.getSecurityConstraints() != null) {
         projectResource.set("dcmp:securityConstraints", securityToResource(
               project.getSecurityConstraints(), uri, "dcmp:securityConstraints"));
      }
      if (project.getSecurityDefaults() != null) {
         projectResource.set("dcmp:securityDefaults", securityToResource(
               project.getSecurityDefaults(), uri, "dcmp:securityDefaults"));
      }
      projectResource.set("dcmp:modelLevelSecurityEnabled", project.isModelLevelSecurityEnabled());
      if (project.getSecurityDefaults() != null) {
         projectResource.set("dcmp:visibleSecurityConstraints", securityToResource(
               project.getVisibleSecurityConstraints(), uri, "dcmp:visibleSecurityConstraints"));
      }

      projectResource.setUri(uri);
      return projectResource;
   }
   
   
   /**
    * 
    * @param model uses its project
    * @param modelResource if not null, is updated
    * @return
    * @throws ResourceParsingException
    */
   public DCResource modelToResource(DCModelBase model, DCResource modelResource) throws ResourceParsingException {
      if (modelResource == null) {
         modelResource = DCResource.create(null, ResourceModelIniter.MODEL_MODEL_NAME);
      }
      
      DCModelBase definitionModel = dataModelService.getDefinitionModel(model); // uses its project
      DCModelBase storageModel = dataModelService.getStorageModel(model); // null if abstract ; uses its project
      
      DCSecurity security = model.getSecurity();
      
      // filling model's provided props :
      modelResource
            .set("dcmo:name", model.getName())
            .set("dcmo:pointOfViewAbsoluteName", model.getPointOfViewAbsoluteName())
            .set("dcmo:majorVersion", model.getMajorVersion())
            // NB. NOT o:version which is only in DCModelBase for info purpose
            
             // POLY
            .set("dcmo:isDefinition", model.isDefinition()) // = !dcmo:isStorageOnly
            .set("dcmo:isStorage", model.isStorage())
            .set("dcmo:isInstanciable", model.isInstanciable())
            .set("dcmo:isMultiProjectStorage", model.isMultiProjectStorage())
         
            // NB. minor version is Resource version
            .set("dcmo:documentation", model.getDocumentation())// TODO in another collection for performance
            
            // storage :
            ///.set("dcmo:collectionName", model.getCollectionName()) // POLY RATHER storageModel.getName()
            .set("dcmo:maxScan", model.getMaxScan())
            
            // TODO if instanciable :
            .set("dcmo:isHistorizable", model.isHistorizable())
            .set("dcmo:isContributable", model.isContributable())
            
            // POLY cache :
            .set("dcmo:definitionModel", definitionModel.getName()) // all models should have one !!
            ;
      if (storageModel != null) {
         modelResource.set("dcmo:storageModel", storageModel.getName());
      }
      securityToResource(security, modelResource);
      if (model.getVersion() >= 0) {
         modelResource.setVersion(model.getVersion());
      } // not at creation, rather update DCModel.version from its resource after each put

      // once id source props are complete, build URI out of them :
      String uri = this.buildModelUri(modelResource);
      modelResource.setUri(uri);
      
      this.modelFieldsAndMixinsToResource(model, modelResource);
      
      return modelResource;
   }

   private void securityToResource(DCSecurity security, DCResource modelResource) {
      modelResource
         // (writing set as list else is json map)
         .set("dcms:isAuthentifiedReadable", (security != null) ? security.isAuthentifiedReadable() : null)
         .set("dcms:isAuthentifiedCreatable", (security != null) ? security.isAuthentifiedCreatable() : null)
         .set("dcms:isAuthentifiedWritable", (security != null) ? security.isAuthentifiedWritable() : null)
         .set("dcms:resourceCreators", (security != null) ? new ArrayList<String>(security.getResourceCreators()) : null)
         .set("dcms:resourceCreationOwners", (security != null) ? new ArrayList<String>(security.getResourceCreationOwners()) : null)
         .set("dcms:resourceReaders", (security != null) ? new ArrayList<String>(security.getResourceReaders()) : null)
         .set("dcms:resourceWriters", (security != null) ? new ArrayList<String>(security.getResourceWriters()) : null)
         .set("dcms:resourceOwners", (security != null) ? new ArrayList<String>(security.getResourceOwners()) : null);
   }
   private Object securityToResource(DCSecurity securityConstraints, String uri, String subId) {
      try {
         DCResource securityResource = DCResource.create(SimpleUriService.getContainerUrl(),
               ResourceModelIniter.MODEL_SECURITY_NAME, UriHelper.parseUri(uri), subId);
         securityResource.setVersion(0l); // dummy (OR should be top level's ??)
         securityToResource(securityConstraints, securityResource);
         return securityResource;
      } catch (URISyntaxException | MalformedURLException ex) {
         throw new RuntimeException(ex);
      }
   }
   
   /**
    * Does not use modelService or any project
    * @param model
    * @param modelResource
    * @throws ResourceParsingException
    */
   private void modelFieldsAndMixinsToResource(DCModelBase model, DCResource modelResource) throws ResourceParsingException {
      // still fill other props, including uri-depending ones :
      DCURI dcUri;
      try {
         dcUri = UriHelper.parseUri(modelResource.getUri());
      } catch (MalformedURLException | URISyntaxException e) {
         throw new RuntimeException(e);
      }
      String fieldUriPrefix = this.buildFieldUriPrefix(dcUri);
      // allow to enrich existing fields' props :
      @SuppressWarnings("unchecked")
      List<Map<String, Object>> existingFieldsProps = (List<Map<String, Object>>) modelResource.get("dcmo:fields");
      modelResource.set("dcmo:fields", fieldsToProps(model.getFieldMap(),
            fieldUriPrefix, existingFieldsProps));
      
      ImmutableList.Builder<Object> mixinsPropBuilder = DCResource.listBuilder();
      for (DCModelBase mixin : model.getMixins()) {
         mixinsPropBuilder.add(mixin.getName());
      }
      modelResource.set("dcmo:mixins", mixinsPropBuilder.build());
      
      // fieldsAndMixinNames (to allow to rebuild them in DCModel in the right order,
      // and for introspection) :
      modelResource.set("dcmo:fieldAndMixins", new ArrayList<String>(model.getFieldAndMixinNames())); // ordered set
      
      // caches (so that they can be queried for introspection as well) :
      modelResource.set("dcmo:globalMixins",
            new ArrayList<String>(model.getGlobalMixinNames())); // TODO order
      modelResource.set("dcmo:globalFields", fieldsToProps(model.getGlobalFieldMap(),
            fieldUriPrefix, null)); // computed, so don't enrich existing props
   }
   
   
   /** public for tests 
    * @throws ResourceParsingException */
   // TODO move to ModelResourceMappingService & modelAdminService.addModel() !
   public ImmutableList<Object> fieldsToProps(Map<String, DCField> mapFields,
         String fieldUriPrefix, List<Map<String, Object>> existingFieldsProps) throws ResourceParsingException {
      ImmutableList.Builder<Object> fieldsPropBuilder = DCResource.listBuilder();
      for (String fieldName : mapFields.keySet()) { // NB. ordered
         DCField field = mapFields.get(fieldName);
         Map<String,Object> existingFieldProps = (existingFieldsProps != null)
               ? findFieldProps(fieldName, existingFieldsProps) : null;
         fieldsPropBuilder.add(fieldToProps(field, fieldUriPrefix, existingFieldProps));
      }
      return fieldsPropBuilder.build();
   }
   
   private Map<String,Object> findFieldProps(String fieldName,
         List<Map<String, Object>> existingFieldsProps) {
      for (Map<String, Object> existingFieldProps : existingFieldsProps) {
         if (fieldName.equals(existingFieldProps.get("dcmf:name"))) {
            return existingFieldProps;
         }
      }
      return null;
   }

   /**
    * public for tests 
    * @param field
    * @param fieldUriPrefix
    * @param existingProps
    * @return
    * @throws ResourceParsingException
    */
   // TODO move to ModelResourceMappingService & modelAdminService.addModel() !
   public /*Immutable*/Map<String, Object> fieldToProps(DCField field,
         String fieldUriPrefix, Map<String,Object> existingProps) throws ResourceParsingException {
      // building (dummy ?) field uri :
      //String uri = aboveUri + '/' + field.getName();
      // NO TODO pb uri must be of its own type and not its container's
      String fieldUri = fieldUriPrefix + '/' + field.getName();
      // TODO also for lists : /i
      //ImmutableMap.Builder<String, Object> fieldPropBuilder = DCResource.propertiesBuilder();
      DCResource.MapBuilder<String, Object> fieldPropBuilder;
      if (existingProps != null) {
         fieldPropBuilder = new DCResource.MapBuilder<String,Object>(existingProps);
      } else {
         fieldPropBuilder = new DCResource.MapBuilder<String,Object>();
      }
      fieldPropBuilder
            //.put("@id", field.getName().replaceAll(":", "__")
            //      .replaceAll("[!?]", "")) // relative (?), TODO TODO better solution for '!' start
            .put("@id", fieldUri)
            .put("@type", DCResource.listBuilder().add(ResourceModelIniter.MODEL_FIELD_NAME).build()) // OR default one ?
            .put("o:version", 0l) // dummy (OR should be top level's ??)
            .put("dcmf:name", field.getName())
            .put("dcmf:type", field.getType())
            .put("dcmf:required", field.isRequired()) // TODO not for list ; for map ?!?
            .put("dcmf:queryLimit", field.getQueryLimit()); // TODO not for list, map
      if (field.getAliasedStorageNames() != null) {
         fieldPropBuilder.put("dcmf:aliasedStorageNames", new ArrayList<String>(field.getAliasedStorageNames()));
         // (written as list else json map)
      }
      fieldPropBuilder.put("dcmf:readonly", field.isReadonly());
      String defaultStringValue = valueService.valueToString(field.getDefaultValue()); // TODO or not for list & map ?!?
      if (defaultStringValue != null) {
         fieldPropBuilder.put("dcmf:defaultStringValue", defaultStringValue);
      } // else null means no value, but ImmutableMap.Builder doesn't accept null
      switch(field.getType()) {
      case "map" :
         // NB. including DCI18nField's submap
         DCMapField mapField = (DCMapField) field;
         @SuppressWarnings("unchecked")
         List<Map<String,Object>> existingMapFieldProps = (existingProps != null)
               ? (List<Map<String,Object>>) existingProps.get("dcmf:mapFields") : null;
         fieldPropBuilder.put("dcmf:mapFields",
               fieldsToProps(mapField.getMapFields(), fieldUri, existingMapFieldProps));
         break;
      case "i18n" :
         fieldPropBuilder.put("dcmf:defaultLanguage", ((DCI18nField) field).getDefaultLanguage());
      case "list" : // and also i18n
         @SuppressWarnings("unchecked")
         Map<String,Object> existingListElementFieldProps = (existingProps != null)
            ? (Map<String,Object>) existingProps.get("dcmf:listElementField") : null;
         fieldPropBuilder.put("dcmf:listElementField", fieldToProps(
               ((DCListField) field).getListElementField(), fieldUri, existingListElementFieldProps));
         break;
      case "resource" :
         fieldPropBuilder.put("dcmf:resourceType", ((DCResourceField) field).getResourceType());
         // TODO LATER OPT also embedded Resource as map
         break;
      }
      return fieldPropBuilder.build();
   }
   
   
   
   //////////////////////////////////////////////////////
   // Resource to DCModel (used in ModelResourceDCListener) :

   /**
    * (not done in the context of another / current project)
    * @param r
    * @return
    * @throws ResourceException if can't find local model or visible project
    * @throws URISyntaxException 
    * @throws MalformedURLException 
    */
   public DCProject toProject(DCResource r) throws ResourceException, MalformedURLException, URISyntaxException {
      // project is always be oasis.main for projects
      //String pointOfViewAbsoluteName = dataModelService.getProject().getAbsoluteName();
      String name = (String) r.get(ResourceModelIniter.POINTOFVIEW_NAME_PROP);
      DCProject project = new DCProject(name);
      return toProject(r, project);
   }
   public DCProject toProject(DCResource r, DCProject project) throws ResourceException, MalformedURLException, URISyntaxException {
      // project is always be oasis.main for projects
      //String pointOfViewAbsoluteName = dataModelService.getProject().getAbsoluteName();

      Long version = r.getVersion();
      if (version == null) {
         version = -1l; // assuming new
      }
      project.setVersion(version); // only in DCModelBase for info purpose
      // LET MAJOR VERSION & UNVERSIONED NAME BE PARSED FROM NAME
      /*try {
         // parsing major version : (could be Long if locally built ex. in model listener)
         Long majorVersion = valueService.parseLong(r.get("dcmpv:majorVersion"), null);
         if (majorVersion == null) {
            majorVersion = 0l; // TODO LATER compute & check : parse from dcmo:name
         }
         project.setMajorVersion(majorVersion); // LATER
      } catch (ResourceParsingException rpex) {
         throw new ResourceException("Error parsing project", rpex, r, project);
      }
      if (r.get("dcmpv:unversionedName") != null) {
         project.setUnversionedName((String) r.get("dcmpv:unversionedName"));
      }*/
      
      @SuppressWarnings("unchecked")
      //List<Map<String, Object>> visibleProjects = (List<Map<String, Object>>) r.get("dcmp:visibleProjects");
      List<String> visibleProjectUris = (List<String>) r.get("dcmp:localVisibleProjects");
      if (visibleProjectUris != null) {
         //for (Map<String, Object> visibleProject : visibleProjects) {
         for (String visibleProjectUri : visibleProjectUris) {
            String visibleProjectName = UriHelper.parseUri(visibleProjectUri).getId();
            DCProject visibleProject = dataModelService.getProject(visibleProjectName);
            if (visibleProject == null) {
               throw new ResourceException("Can't find visibleProject " + visibleProjectName,
                     null, r, project);
            }
            project.addLocalVisibleProject(visibleProject);
            // TODO isDef/Storage...
            /*boolean vprName = vpr.get("dcmvp:name");
            DCVisibleProject dcVisibleProject = new DCVisibleProject();
            boolean isStorage = vpr.get("dcmvp:isStorage");*/
         }
      } // else allowing model without visible projects ex. Phase 1 sandbox

      // TODO POV ?!
      
      // NB. local models loaded independently in LoadPersistedModelsAtInit
      // TODO LATER local models : use project.localModels to conf relationship
      // (their fork modes i.e. description of what must be done to achieve the fork)

      @SuppressWarnings("unchecked")
      List<String> forkedUris = (List<String>) r.get("dcmp:forkedUris");
      project.setForkedUris(new HashSet<String>(forkedUris));
      @SuppressWarnings("unchecked")
      List<String> frozenModelNames = (List<String>) r.get("dcmp:frozenModelNames");
      project.setFrozenModelNames(new HashSet<String>(frozenModelNames));
      project.setModelLevelSecurityEnabled((boolean) r.get("dcmp:modelLevelSecurityEnabled"));
      
      return project;
   }
   
   /**
    * Uses model resource's pointOfViewAbsoluteName as project BUT must be currentProjectName.
    * Creates and fills DCModel or DCMixin (by calling resourceToFieldsAndMixins())
    * from given resource.
    * BUT does not clean / update r with ex. DCModelBase-computed fields (globalFields...
    * for this call afterwards modelFieldsAndMixinsToResource()), nor check consistency
    * (country / language specific model... for this call afterwards checkModelOrMixin()).
    * 
    * TODO LATER gather all ResourceParsingException in a ParsingContext
    * like in ResourceEntityMappingService 
    * @param r
    * @param isModel<commit_msg>patched project.frozenModelNames etc. mapping in case of old projects<commit_after>package org.oasis.datacore.model.resource;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.oasis.datacore.core.meta.SimpleUriService;
import org.oasis.datacore.core.meta.model.DCField;
import org.oasis.datacore.core.meta.model.DCI18nField;
import org.oasis.datacore.core.meta.model.DCListField;
import org.oasis.datacore.core.meta.model.DCMapField;
import org.oasis.datacore.core.meta.model.DCModel;
import org.oasis.datacore.core.meta.model.DCModelBase;
import org.oasis.datacore.core.meta.model.DCModelService;
import org.oasis.datacore.core.meta.model.DCResourceField;
import org.oasis.datacore.core.meta.model.DCSecurity;
import org.oasis.datacore.core.meta.pov.DCProject;
import org.oasis.datacore.rest.api.DCResource;
import org.oasis.datacore.rest.api.util.DCURI;
import org.oasis.datacore.rest.api.util.UriHelper;
import org.oasis.datacore.rest.server.parsing.exception.ResourceParsingException;
import org.oasis.datacore.rest.server.parsing.model.DCResourceParsingContext;
import org.oasis.datacore.rest.server.resource.ResourceEntityMapperService;
import org.oasis.datacore.rest.server.resource.ResourceException;
import org.oasis.datacore.rest.server.resource.ValueParsingService;
import org.oasis.datacore.sample.ResourceModelIniter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;

@Component
public class ModelResourceMappingService {
   
   @Autowired
   private DCModelService dataModelService;
   @Autowired
   private ValueParsingService valueService;
   /** to parse i18n default value according to default language
    * LATER use it in the whole model mapping process ? */
   @Autowired
   private ResourceEntityMapperService resourceEntityMapperService;
   
   
   //////////////////////////////////////////////////////
   // DCModel to Resource (used in ResourceModelIniter) :

   /** public for tests */
   public String buildModelUri(DCResource modelResource) {
      return SimpleUriService.buildModelUri(
            (String) modelResource.get("dcmo:name")/* + '_' + modelResource.get("dcmo:majorVersion")*/); // LATER refactor
   }
   /** only for tests */
   public String buildModelUri(DCModelBase model) {
      return SimpleUriService.buildModelUri(
            (String) model.getName()/* + '_' + model.getMajorVersion()*/); // LATER refactor
   }
   
   /** public for tests */
   public String buildFieldUriPrefix(DCURI dcUri) {
      return SimpleUriService.buildUri(ResourceModelIniter.MODEL_FIELD_NAME,
            dcUri.getType() + '/' + dcUri.getId()).toString();
   }

   /**
    * Used by all POVs
    * @param modelType
    * @param name
    * @return
    */
   public String buildNameUri(String modelType, String name) {
      return SimpleUriService.buildUri(modelType, name);
   }
   
   /**
    * NOT USED
    * public for tests
    * @param projectResource
    * @return
    * @throws RuntimeException if missing model type, TODO better ex. ResourceParsingException
    */
   public String buildPointOfViewUri(DCResource projectResource) throws RuntimeException {
      List<String> types = projectResource.getTypes();
      if (types == null || types.isEmpty()) {
         throw new RuntimeException("Missing Model type");
      }
      return SimpleUriService.buildUri(types.get(0),
            (String) projectResource.get(ResourceModelIniter.POINTOFVIEW_NAME_PROP));
   }

   /**
    * 
    * @param project
    * @return
    * @throws ResourceParsingException
    */
   public DCResource projectToResource(DCProject project) throws ResourceParsingException {
      DCResource projectResource = DCResource.create(null, ResourceModelIniter.MODEL_PROJECT_NAME);
      return projectToResource(project, projectResource);
   }
   public DCResource projectToResource(DCProject project, DCResource projectResource) throws ResourceParsingException {
      projectResource
            .set("dcmpv:name", project.getName())
            .set("dcmpv:majorVersion", project.getMajorVersion())
            .set("dcmpv:unversionedName", project.getUnversionedName())
            // NB. NOT o:version which is only in DCProject for info purpose
         
            // NB. minor version is Resource version
            .set("dcmpv:documentation", project.getDocumentation())// TODO in another collection for performance
            
            // POLY cache : TODO
            ;
      
      String uri = this.buildPointOfViewUri(projectResource);

      //projectResource.set("dcmpv:pointOfViews", pointOfViewsToNames(project.getLocalVisibleProjects())); // TODO as cache ?!
      projectResource.set("dcmp:localVisibleProjects", project.getLocalVisibleProjects().stream()
            .map(p -> this.buildNameUri(ResourceModelIniter.MODEL_PROJECT_NAME, p.getName())).collect(Collectors.toList())); // NOT toSet else JSON'd as HashMap
      projectResource.set("dcmp:visibleProjectNames", new ArrayList<String>(
            dataModelService.getVisibleProjectNames(project.getName()))); // only to display for now
      projectResource.set("dcmp:forkedUris", new ArrayList<String>(
            project.getForkedUris())); // only to display for now
      projectResource.set("dcmp:frozenModelNames", new ArrayList<String>(
            project.getFrozenModelNames()));
      projectResource.set("dcmp:useCasePointOfViews", project.getUseCasePointOfViews().stream()
            .map(ucpov -> {
               //useCasePointOfViewToResource(ucpov); // TODO TODO & useCasePointOfViewElementToResource(ucpovelt)
               //ucpov.getPointOfViews().stream().map(ucpovelt -> mrMappingService.buildPointOfViewUri(ucpovelt)).collect(Collectors.toList())); // NOT toSet else JSON'd as HashMap
               return this.buildNameUri(ResourceModelIniter.MODEL_POINTOFVIEW_NAME, ucpov.getName());
            }).collect(Collectors.toList())); // NOT toSet else JSON'd as HashMap
      if (project.getSecurityConstraints() != null) {
         projectResource.set("dcmp:securityConstraints", securityToResource(
               project.getSecurityConstraints(), uri, "dcmp:securityConstraints"));
      }
      if (project.getSecurityDefaults() != null) {
         projectResource.set("dcmp:securityDefaults", securityToResource(
               project.getSecurityDefaults(), uri, "dcmp:securityDefaults"));
      }
      projectResource.set("dcmp:modelLevelSecurityEnabled", project.isModelLevelSecurityEnabled());
      if (project.getSecurityDefaults() != null) {
         projectResource.set("dcmp:visibleSecurityConstraints", securityToResource(
               project.getVisibleSecurityConstraints(), uri, "dcmp:visibleSecurityConstraints"));
      }

      projectResource.setUri(uri);
      return projectResource;
   }
   
   
   /**
    * 
    * @param model uses its project
    * @param modelResource if not null, is updated
    * @return
    * @throws ResourceParsingException
    */
   public DCResource modelToResource(DCModelBase model, DCResource modelResource) throws ResourceParsingException {
      if (modelResource == null) {
         modelResource = DCResource.create(null, ResourceModelIniter.MODEL_MODEL_NAME);
      }
      
      DCModelBase definitionModel = dataModelService.getDefinitionModel(model); // uses its project
      DCModelBase storageModel = dataModelService.getStorageModel(model); // null if abstract ; uses its project
      
      DCSecurity security = model.getSecurity();
      
      // filling model's provided props :
      modelResource
            .set("dcmo:name", model.getName())
            .set("dcmo:pointOfViewAbsoluteName", model.getPointOfViewAbsoluteName())
            .set("dcmo:majorVersion", model.getMajorVersion())
            // NB. NOT o:version which is only in DCModelBase for info purpose
            
             // POLY
            .set("dcmo:isDefinition", model.isDefinition()) // = !dcmo:isStorageOnly
            .set("dcmo:isStorage", model.isStorage())
            .set("dcmo:isInstanciable", model.isInstanciable())
            .set("dcmo:isMultiProjectStorage", model.isMultiProjectStorage())
         
            // NB. minor version is Resource version
            .set("dcmo:documentation", model.getDocumentation())// TODO in another collection for performance
            
            // storage :
            ///.set("dcmo:collectionName", model.getCollectionName()) // POLY RATHER storageModel.getName()
            .set("dcmo:maxScan", model.getMaxScan())
            
            // TODO if instanciable :
            .set("dcmo:isHistorizable", model.isHistorizable())
            .set("dcmo:isContributable", model.isContributable())
            
            // POLY cache :
            .set("dcmo:definitionModel", definitionModel.getName()) // all models should have one !!
            ;
      if (storageModel != null) {
         modelResource.set("dcmo:storageModel", storageModel.getName());
      }
      securityToResource(security, modelResource);
      if (model.getVersion() >= 0) {
         modelResource.setVersion(model.getVersion());
      } // not at creation, rather update DCModel.version from its resource after each put

      // once id source props are complete, build URI out of them :
      String uri = this.buildModelUri(modelResource);
      modelResource.setUri(uri);
      
      this.modelFieldsAndMixinsToResource(model, modelResource);
      
      return modelResource;
   }

   private void securityToResource(DCSecurity security, DCResource modelResource) {
      modelResource
         // (writing set as list else is json map)
         .set("dcms:isAuthentifiedReadable", (security != null) ? security.isAuthentifiedReadable() : null)
         .set("dcms:isAuthentifiedCreatable", (security != null) ? security.isAuthentifiedCreatable() : null)
         .set("dcms:isAuthentifiedWritable", (security != null) ? security.isAuthentifiedWritable() : null)
         .set("dcms:resourceCreators", (security != null) ? new ArrayList<String>(security.getResourceCreators()) : null)
         .set("dcms:resourceCreationOwners", (security != null) ? new ArrayList<String>(security.getResourceCreationOwners()) : null)
         .set("dcms:resourceReaders", (security != null) ? new ArrayList<String>(security.getResourceReaders()) : null)
         .set("dcms:resourceWriters", (security != null) ? new ArrayList<String>(security.getResourceWriters()) : null)
         .set("dcms:resourceOwners", (security != null) ? new ArrayList<String>(security.getResourceOwners()) : null);
   }
   private Object securityToResource(DCSecurity securityConstraints, String uri, String subId) {
      try {
         DCResource securityResource = DCResource.create(SimpleUriService.getContainerUrl(),
               ResourceModelIniter.MODEL_SECURITY_NAME, UriHelper.parseUri(uri), subId);
         securityResource.setVersion(0l); // dummy (OR should be top level's ??)
         securityToResource(securityConstraints, securityResource);
         return securityResource;
      } catch (URISyntaxException | MalformedURLException ex) {
         throw new RuntimeException(ex);
      }
   }
   
   /**
    * Does not use modelService or any project
    * @param model
    * @param modelResource
    * @throws ResourceParsingException
    */
   private void modelFieldsAndMixinsToResource(DCModelBase model, DCResource modelResource) throws ResourceParsingException {
      // still fill other props, including uri-depending ones :
      DCURI dcUri;
      try {
         dcUri = UriHelper.parseUri(modelResource.getUri());
      } catch (MalformedURLException | URISyntaxException e) {
         throw new RuntimeException(e);
      }
      String fieldUriPrefix = this.buildFieldUriPrefix(dcUri);
      // allow to enrich existing fields' props :
      @SuppressWarnings("unchecked")
      List<Map<String, Object>> existingFieldsProps = (List<Map<String, Object>>) modelResource.get("dcmo:fields");
      modelResource.set("dcmo:fields", fieldsToProps(model.getFieldMap(),
            fieldUriPrefix, existingFieldsProps));
      
      ImmutableList.Builder<Object> mixinsPropBuilder = DCResource.listBuilder();
      for (DCModelBase mixin : model.getMixins()) {
         mixinsPropBuilder.add(mixin.getName());
      }
      modelResource.set("dcmo:mixins", mixinsPropBuilder.build());
      
      // fieldsAndMixinNames (to allow to rebuild them in DCModel in the right order,
      // and for introspection) :
      modelResource.set("dcmo:fieldAndMixins", new ArrayList<String>(model.getFieldAndMixinNames())); // ordered set
      
      // caches (so that they can be queried for introspection as well) :
      modelResource.set("dcmo:globalMixins",
            new ArrayList<String>(model.getGlobalMixinNames())); // TODO order
      modelResource.set("dcmo:globalFields", fieldsToProps(model.getGlobalFieldMap(),
            fieldUriPrefix, null)); // computed, so don't enrich existing props
   }
   
   
   /** public for tests 
    * @throws ResourceParsingException */
   // TODO move to ModelResourceMappingService & modelAdminService.addModel() !
   public ImmutableList<Object> fieldsToProps(Map<String, DCField> mapFields,
         String fieldUriPrefix, List<Map<String, Object>> existingFieldsProps) throws ResourceParsingException {
      ImmutableList.Builder<Object> fieldsPropBuilder = DCResource.listBuilder();
      for (String fieldName : mapFields.keySet()) { // NB. ordered
         DCField field = mapFields.get(fieldName);
         Map<String,Object> existingFieldProps = (existingFieldsProps != null)
               ? findFieldProps(fieldName, existingFieldsProps) : null;
         fieldsPropBuilder.add(fieldToProps(field, fieldUriPrefix, existingFieldProps));
      }
      return fieldsPropBuilder.build();
   }
   
   private Map<String,Object> findFieldProps(String fieldName,
         List<Map<String, Object>> existingFieldsProps) {
      for (Map<String, Object> existingFieldProps : existingFieldsProps) {
         if (fieldName.equals(existingFieldProps.get("dcmf:name"))) {
            return existingFieldProps;
         }
      }
      return null;
   }

   /**
    * public for tests 
    * @param field
    * @param fieldUriPrefix
    * @param existingProps
    * @return
    * @throws ResourceParsingException
    */
   // TODO move to ModelResourceMappingService & modelAdminService.addModel() !
   public /*Immutable*/Map<String, Object> fieldToProps(DCField field,
         String fieldUriPrefix, Map<String,Object> existingProps) throws ResourceParsingException {
      // building (dummy ?) field uri :
      //String uri = aboveUri + '/' + field.getName();
      // NO TODO pb uri must be of its own type and not its container's
      String fieldUri = fieldUriPrefix + '/' + field.getName();
      // TODO also for lists : /i
      //ImmutableMap.Builder<String, Object> fieldPropBuilder = DCResource.propertiesBuilder();
      DCResource.MapBuilder<String, Object> fieldPropBuilder;
      if (existingProps != null) {
         fieldPropBuilder = new DCResource.MapBuilder<String,Object>(existingProps);
      } else {
         fieldPropBuilder = new DCResource.MapBuilder<String,Object>();
      }
      fieldPropBuilder
            //.put("@id", field.getName().replaceAll(":", "__")
            //      .replaceAll("[!?]", "")) // relative (?), TODO TODO better solution for '!' start
            .put("@id", fieldUri)
            .put("@type", DCResource.listBuilder().add(ResourceModelIniter.MODEL_FIELD_NAME).build()) // OR default one ?
            .put("o:version", 0l) // dummy (OR should be top level's ??)
            .put("dcmf:name", field.getName())
            .put("dcmf:type", field.getType())
            .put("dcmf:required", field.isRequired()) // TODO not for list ; for map ?!?
            .put("dcmf:queryLimit", field.getQueryLimit()); // TODO not for list, map
      if (field.getAliasedStorageNames() != null) {
         fieldPropBuilder.put("dcmf:aliasedStorageNames", new ArrayList<String>(field.getAliasedStorageNames()));
         // (written as list else json map)
      }
      fieldPropBuilder.put("dcmf:readonly", field.isReadonly());
      String defaultStringValue = valueService.valueToString(field.getDefaultValue()); // TODO or not for list & map ?!?
      if (defaultStringValue != null) {
         fieldPropBuilder.put("dcmf:defaultStringValue", defaultStringValue);
      } // else null means no value, but ImmutableMap.Builder doesn't accept null
      switch(field.getType()) {
      case "map" :
         // NB. including DCI18nField's submap
         DCMapField mapField = (DCMapField) field;
         @SuppressWarnings("unchecked")
         List<Map<String,Object>> existingMapFieldProps = (existingProps != null)
               ? (List<Map<String,Object>>) existingProps.get("dcmf:mapFields") : null;
         fieldPropBuilder.put("dcmf:mapFields",
               fieldsToProps(mapField.getMapFields(), fieldUri, existingMapFieldProps));
         break;
      case "i18n" :
         fieldPropBuilder.put("dcmf:defaultLanguage", ((DCI18nField) field).getDefaultLanguage());
      case "list" : // and also i18n
         @SuppressWarnings("unchecked")
         Map<String,Object> existingListElementFieldProps = (existingProps != null)
            ? (Map<String,Object>) existingProps.get("dcmf:listElementField") : null;
         fieldPropBuilder.put("dcmf:listElementField", fieldToProps(
               ((DCListField) field).getListElementField(), fieldUri, existingListElementFieldProps));
         break;
      case "resource" :
         fieldPropBuilder.put("dcmf:resourceType", ((DCResourceField) field).getResourceType());
         // TODO LATER OPT also embedded Resource as map
         break;
      }
      return fieldPropBuilder.build();
   }
   
   
   
   //////////////////////////////////////////////////////
   // Resource to DCModel (used in ModelResourceDCListener) :

   /**
    * (not done in the context of another / current project)
    * @param r
    * @return
    * @throws ResourceException if can't find local model or visible project
    * @throws URISyntaxException 
    * @throws MalformedURLException 
    */
   public DCProject toProject(DCResource r) throws ResourceException, MalformedURLException, URISyntaxException {
      // project is always be oasis.main for projects
      //String pointOfViewAbsoluteName = dataModelService.getProject().getAbsoluteName();
      String name = (String) r.get(ResourceModelIniter.POINTOFVIEW_NAME_PROP);
      DCProject project = new DCProject(name);
      return toProject(r, project);
   }
   public DCProject toProject(DCResource r, DCProject project) throws ResourceException, MalformedURLException, URISyntaxException {
      // project is always be oasis.main for projects
      //String pointOfViewAbsoluteName = dataModelService.getProject().getAbsoluteName();

      Long version = r.getVersion();
      if (version == null) {
         version = -1l; // assuming new
      }
      project.setVersion(version); // only in DCModelBase for info purpose
      // LET MAJOR VERSION & UNVERSIONED NAME BE PARSED FROM NAME
      /*try {
         // parsing major version : (could be Long if locally built ex. in model listener)
         Long majorVersion = valueService.parseLong(r.get("dcmpv:majorVersion"), null);
         if (majorVersion == null) {
            majorVersion = 0l; // TODO LATER compute & check : parse from dcmo:name
         }
         project.setMajorVersion(majorVersion); // LATER
      } catch (ResourceParsingException rpex) {
         throw new ResourceException("Error parsing project", rpex, r, project);
      }
      if (r.get("dcmpv:unversionedName") != null) {
         project.setUnversionedName((String) r.get("dcmpv:unversionedName"));
      }*/
      
      @SuppressWarnings("unchecked")
      //List<Map<String, Object>> visibleProjects = (List<Map<String, Object>>) r.get("dcmp:visibleProjects");
      List<String> visibleProjectUris = (List<String>) r.get("dcmp:localVisibleProjects");
      if (visibleProjectUris != null) {
         //for (Map<String, Object> visibleProject : visibleProjects) {
         for (String visibleProjectUri : visibleProjectUris) {
            String visibleProjectName = UriHelper.parseUri(visibleProjectUri).getId();
            DCProject visibleProject = dataModelService.getProject(visibleProjectName);
            if (visibleProject == null) {
               throw new ResourceException("Can't find visibleProject " + visibleProjectName,
                     null, r, project);
            }
            project.addLocalVisibleProject(visibleProject);
            // TODO isDef/Storage...
            /*boolean vprName = vpr.get("dcmvp:name");
            DCVisibleProject dcVisibleProject = new DCVisibleProject();
            boolean isStorage = vpr.get("dcmvp:isStorage");*/
         }
      } // else allowing model without visible projects ex. Phase 1 sandbox

      // TODO POV ?!
      
      // NB. local models loaded independently in LoadPersistedModelsAtInit
      // TODO LATER local models : use project.localModels to conf relationship
      // (their fork modes i.e. description of what must be done to achieve the fork)

      @SuppressWarnings("unchecked")
      List<String> forkedUris = (List<String>) r.get("dcmp:forkedUris");
      project.setForkedUris((forkedUris == null ? new HashSet<String>() // in case of old project without it
            : new HashSet<String>(forkedUris)));
      @SuppressWarnings("unchecked")
      List<String> frozenModelNames = (List<String>) r.get("dcmp:frozenModelNames");
      project.setFrozenModelNames((frozenModelNames == null ? new HashSet<String>() // in case of old project without it
            : new HashSet<String>(frozenModelNames)));
      Object isModelLevelSecurityEnabledFound = r.get("dcmp:modelLevelSecurityEnabled");
      if (isModelLevelSecurityEnabledFound != null) {
         project.setModelLevelSecurityEnabled((boolean) isModelLevelSecurityEnabledFound);
      } // otherwise old project without it
      
      return project;
   }
   
   /**
    * Uses model resource's pointOfViewAbsoluteName as project BUT must be currentProjectName.
    * Creates and fills DCModel or DCMixin (by calling resourceToFieldsAndMixins())
    * from given resource.
    * BUT does not clean / update r with ex. DCModelBase-computed fields (globalFields...
    * for this call afterwards modelFieldsAndMixinsToResource()), nor check consistency
    * (country / language specific model... for this call afterwards checkModelOrMixin()).
    * 
    * TODO LATER gather all ResourceParsingException in a ParsingContext
    * like in ResourceEntityMappingService 
    * @param r
    * @param isModel
{'fraction_non_alphanumeric': 0.11341079378868499, 'fraction_numerical': 0.0008252796177652296, 'mean_word_length': 3.1731013231828893, 'pattern_counts': {'":': 2, '<': 109, '<?xml version=': 0, '>': 117, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5941730', 'n_tokens_mistral': 12577, 'n_tokens_neox': 12553, 'n_words': 2750}