/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.stanbol.entityhub.jersey.parsers;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.servlet.ServletContext;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;

import org.apache.commons.io.IOUtils;
import org.apache.stanbol.commons.namespaceprefix.NamespaceMappingUtils;
import org.apache.stanbol.commons.namespaceprefix.NamespacePrefixService;
import org.apache.stanbol.commons.web.base.ContextHelper;
import org.apache.stanbol.entityhub.core.mapping.ValueConverterFactory;
import org.apache.stanbol.entityhub.core.model.InMemoryValueFactory;
import org.apache.stanbol.entityhub.core.query.FieldQueryImpl;
import org.apache.stanbol.entityhub.ldpath.query.LDPathFieldQueryImpl;
import org.apache.stanbol.entityhub.servicesapi.model.ValueFactory;
import org.apache.stanbol.entityhub.servicesapi.query.Constraint;
import org.apache.stanbol.entityhub.servicesapi.query.Constraint.ConstraintType;
import org.apache.stanbol.entityhub.servicesapi.query.FieldQuery;
import org.apache.stanbol.entityhub.servicesapi.query.RangeConstraint;
import org.apache.stanbol.entityhub.servicesapi.query.ReferenceConstraint;
import org.apache.stanbol.entityhub.servicesapi.query.SimilarityConstraint;
import org.apache.stanbol.entityhub.servicesapi.query.TextConstraint;
import org.apache.stanbol.entityhub.servicesapi.query.TextConstraint.PatternType;
import org.apache.stanbol.entityhub.servicesapi.query.ValueConstraint;
import org.apache.stanbol.entityhub.servicesapi.query.ValueConstraint.MODE;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Provider
public class FieldQueryReader implements MessageBodyReader<FieldQuery> {
    private static final Logger log = LoggerFactory.getLogger(FieldQueryReader.class);
    
    private static final ValueFactory valueFactory = InMemoryValueFactory.getInstance();
    private static final ValueConverterFactory converterFactory = ValueConverterFactory.getDefaultInstance();
    
    private ServletContext context;
    
    public FieldQueryReader(@Context ServletContext context) {
        this.context = context;
    }
    
    private NamespacePrefixService getNsPrefixService(){
        return ContextHelper.getServiceFromContext(NamespacePrefixService.class, context);
    }
    
    @Override
    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        log.debug("isReadable type {}, mediaType {}",type,mediaType);
        return FieldQuery.class.isAssignableFrom(type); //&& mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE);
    }

    @Override
    public FieldQuery readFrom(Class<FieldQuery> type,
                               Type genericType,
                               Annotation[] annotations,
                               MediaType mediaType,
                               MultivaluedMap<String,String> httpHeaders,
                               InputStream entityStream) throws IOException, WebApplicationException {
        String queryString = IOUtils.toString(entityStream);
        log.debug("Parsed QueryString: \n{}",queryString);
        MediaType acceptedType = MediaType.valueOf(httpHeaders.getFirst("Accept"));
        if(acceptedType.isWildcardType()){
            acceptedType = MediaType.TEXT_PLAIN_TYPE;
        }
        try {
            return fromJSON(queryString,acceptedType, getNsPrefixService());
        } catch (JSONException e) {
            log.error("Unable to parse Request ",e);
            StringBuilder message = new StringBuilder();
            message.append("Parsed FieldQuery is not valid JSON\n");
            message.append("Parsed String:\n");
            message.append(queryString);
            log.warn(message.toString());
            //TODO: Jersey wraps Exceptions thrown by MessageBodyReader into
            // other ones. Because of that the Response created by the
            // WebApplicationException is "lost" and the user will get an
            // 500 with no comment and HTML content type :(
            // As a workaround one could use a wrapping object as generic type
            // that parses the error and than throw the Exception within the
            // Resource using this MessageBodyReader
            throw new WebApplicationException(
                Response.status(Status.BAD_REQUEST).
                entity(message.toString()).
                header(HttpHeaders.ACCEPT, acceptedType.toString()).build());
        }
    }
    /**
     * 
     * @param queryFactory
     * @param jsonQueryString
     * @param acceptedMediaType used to add the accept header to Error responses
     * @return
     * @throws JSONException
     * @throws WebApplicationException
     */
    public static FieldQuery fromJSON(String jsonQueryString,MediaType acceptedMediaType, 
                                      NamespacePrefixService nsPrefixService) throws JSONException,WebApplicationException {
        if(jsonQueryString == null){
            throw new IllegalArgumentException("The parsed JSON object MUST NOT be NULL!");
        }
        JSONObject jQuery = new JSONObject(jsonQueryString);
        FieldQuery query;
        if(jQuery.has("ldpath")){ //STANBOL-417: support for using LDPath as select
            LDPathFieldQueryImpl ldPathQuery = new LDPathFieldQueryImpl();
            ldPathQuery.setLDPathSelect(jQuery.getString("ldpath"));
            query = ldPathQuery;
        } else {
            query = new FieldQueryImpl();
        }
        if(!jQuery.has("constraints")){
            StringBuilder message = new StringBuilder();
            message.append("The parsed Field Query MUST contain at least a single 'constraints'\n");
            message.append("Parsed Query:\n");
            message.append(jQuery.toString(4));
            log.warn(message.toString());
            throw new WebApplicationException(
                Response.status(Status.BAD_REQUEST).entity(
                    message.toString()).header(HttpHeaders.ACCEPT, acceptedMediaType.toString())
                    .build());
        }
        JSONArray constraints = jQuery.getJSONArray("constraints");
        //collect all parsing Errors to report a complete set of all errors
        boolean parsingError = false;
        StringBuilder parsingErrorMessages = new StringBuilder();
        parsingErrorMessages.append("Constraint parsing Errors:\n");
        for(int i=0;i<constraints.length();i++){
            JSONObject jConstraint = constraints.getJSONObject(i);
            if(jConstraint.has("field")){
                String field = jConstraint.getString("field");
                //check if there is already a constraint for that field
                if(field == null || field.isEmpty()){
                    parsingErrorMessages.append('\n');
                    parsingErrorMessages.append(
                        "Each Field Query Constraint MUST define a value for 'field'\n");
                    parsingErrorMessages.append("Parsed Constraint:\n");
                    parsingErrorMessages.append(jConstraint.toString(4));
                    parsingErrorMessages.append('\n');
                    parsingError = true;
                    continue;
                }
                String fieldUri = nsPrefixService.getFullName(field);
                if(fieldUri == null){
                    parsingErrorMessages.append('\n');
                    parsingErrorMessages.append(
                        "The 'field' '").append(field).append("uses an unknown namespace prefix '");
                    parsingErrorMessages.append(NamespaceMappingUtils.getPrefix(field)).append("'\n");
                    parsingErrorMessages.append("Parsed Constraint:\n");
                    parsingErrorMessages.append(jConstraint.toString(4));
                    parsingErrorMessages.append('\n');
                    parsingError = true;
                    continue;
                }else if(query.isConstrained(fieldUri)){
                    parsingErrorMessages.append('\n');
                    parsingErrorMessages.append(
                        "The parsed Query defines multiple constraints fr the field '"
                        +fieldUri+"'!\n");
                    parsingErrorMessages.append("FieldQuery allows only a single Constraint for a field\n");
                    parsingErrorMessages.append("Parsed Constraints:\n");
                    parsingErrorMessages.append(constraints.toString(4));
                    parsingErrorMessages.append('\n');
                    parsingError = true;
                    continue;
                } else {
                    try {
                        query.setConstraint(fieldUri, parseConstraint(jConstraint,nsPrefixService));
                    } catch (IllegalArgumentException e) {
                        parsingErrorMessages.append('\n');
                        parsingErrorMessages.append(e.getMessage());
                        parsingErrorMessages.append('\n');
                        parsingError = true;
                        continue;
                    }
                }
            } else { //empty field
                parsingErrorMessages.append('\n');
                parsingErrorMessages.append("Constraints MUST define a value for 'field'\n");
                parsingErrorMessages.append("Parsed Constraint:\n");
                parsingErrorMessages.append(jConstraint.toString(4));
                parsingErrorMessages.append('\n');
                parsingError = true;
                continue;
            }
        }
        if(parsingError){
            String message = parsingErrorMessages.toString();
            log.warn(message);
            throw new WebApplicationException(
                Response.status(Status.BAD_REQUEST).entity(
                    message).header(HttpHeaders.ACCEPT, acceptedMediaType.toString())
                    .build());
        }
        //parse selected fields
        JSONArray selected = jQuery.optJSONArray("selected");
        if(selected != null){
            for(int i=0;i<selected.length();i++){
                String selectedField = selected.getString(i);
                selectedField = nsPrefixService.getFullName(selectedField);
                if(selectedField != null && !selectedField.isEmpty()){
                    query.addSelectedField(selectedField);
                }
            }
        } //else no selected fields -> funny but maybe someone do need only the ids
        //parse limit and offset
        if(jQuery.has("limit") && !jQuery.isNull("limit")){
            try {
                query.setLimit(jQuery.getInt("limit"));
            } catch (JSONException e) {
                parsingErrorMessages.append('\n');
                parsingErrorMessages.append("Property \"limit\" MUST BE a valid integer number!\n");
                parsingErrorMessages.append("Parsed Value:");
                parsingErrorMessages.append(jQuery.get("init"));
                parsingErrorMessages.append('\n');
                parsingError = true;
            }
        }
        if(jQuery.has("offset") && !jQuery.isNull("offset")){
            try {
                query.setOffset(jQuery.getInt("offset"));
            } catch (JSONException e) {
                parsingErrorMessages.append('\n');
                parsingErrorMessages.append("Property \"offset\" MUST BE a valid integer number!\n");
                parsingErrorMessages.append("Parsed Value:");
                parsingErrorMessages.append(jQuery.get("init"));
                parsingErrorMessages.append('\n');
                parsingError = true;
            }
        }
        return query;
    }

    private static Constraint parseConstraint(JSONObject jConstraint, NamespacePrefixService nsPrefixService) throws JSONException {
        if(jConstraint.has("type") && !jConstraint.isNull("type")) {
            String type = jConstraint.getString("type");
            //Event that internally "reference" is not part of the
            //ConstraintType enum it is still present in the serialisation
            //ant the Java API (see ReferenceConstraint class)
            //Value constraints with the dataType Reference and AnyURI are
            //considered to represent reference constraints
            if(type.equals("reference")){
                return parseReferenceConstraint(jConstraint,nsPrefixService);
            } else if (type.equals(ConstraintType.value.name())){
                return parseValueConstraint(jConstraint, nsPrefixService);
            } else if (type.equals(ConstraintType.text.name())){
                return parseTextConstraint(jConstraint);
            } else if (type.equals(ConstraintType.range.name())){
                return parseRangeConstraint(jConstraint,nsPrefixService);
            } else if(type.equals(ConstraintType.similarity.name())){
                return parseSimilarityConstraint(jConstraint, nsPrefixService);
            } else {
                log.warn(String.format("Unknown Constraint Type %s. Supported values are %s",               
                    Arrays.asList("reference",ConstraintType.values())));
                StringBuilder message = new StringBuilder();
                message.append("Parsed Constraint uses an unknown value for 'type'!\n");
                message.append("Supported values: ");
                message.append(ConstraintType.values());
                message.append('\n');
                message.append("Parsed Constraint: \n");
                message.append(jConstraint.toString(4));
                throw new IllegalArgumentException(message.toString());
            }
        } else {
            log.warn(String.format("Earch Constraint MUST HAVE the \"type\" key set to one of the values %s",
                Arrays.asList("reference",ConstraintType.values())));
            StringBuilder message = new StringBuilder();
            message.append("Parsed Constraint does not define a value for the field 'type'!\n");
            message.append("Supported values: ");
            message.append(ConstraintType.values());
            message.append('\n');
            message.append("Parsed Constraint: \n");
            message.append(jConstraint.toString(4));
            throw new IllegalArgumentException(message.toString());
        }
    }

    private static Constraint parseSimilarityConstraint(JSONObject jConstraint, NamespacePrefixService nsPrefixService) throws JSONException {
        String context = jConstraint.optString("context");
        if(context == null){
            throw new IllegalArgumentException("SimilarityConstraints MUST define a \"context\": \n "+jConstraint.toString(4));
        }
        JSONArray addFields = jConstraint.optJSONArray("addFields");
        final List<String> fields;
        if(addFields != null && addFields.length() > 0){
            fields = new ArrayList<String>(addFields.length());
            for(int i=0;i<addFields.length();i++){
                String field = addFields.optString(i);
                field = field != null ? nsPrefixService.getFullName(field) : null;
                if(field != null && !field.isEmpty()){
                    fields.add(field);
                }
            }
        } else {
            fields = null;
        }
        return new SimilarityConstraint(context,fields);
    }

    /**
     * @param jConstraint
     * @return
     * @throws JSONException
     */
    private static Constraint parseRangeConstraint(JSONObject jConstraint, NamespacePrefixService nsPrefixService) throws JSONException {
        Constraint constraint;
        boolean inclusive;
        if(jConstraint.has("inclusive")){
            inclusive = jConstraint.getBoolean("inclusive");
        } else {
            log.debug("RangeConstraint does not define the field 'inclusive'. Use false as default!");
            inclusive = false;
        }
        Object upperBound = jConstraint.opt("upperBound");
        Object lowerBound = jConstraint.opt("lowerBound");
        Collection<String> datatypes = parseDatatypeProperty(jConstraint,nsPrefixService);
        if(datatypes != null && !datatypes.isEmpty()){
            Iterator<String> it = datatypes.iterator();
            String datatype = it.next();
            if(datatypes.size() > 1){ //write warning in case of multiple values
                log.warn("Multiple datatypes are not supported by RangeConstriants!");
                log.warn("  used: {}",datatype);
                while(it.hasNext()){
                    log.warn("  ignored: {}",it.next());
                }
            }
            StringBuilder convertingError = null;
            if(upperBound != null){
                Object convertedUpperBound = converterFactory.convert(upperBound, datatype, valueFactory);
                if(convertedUpperBound == null){
                    log.warn("Unable to convert upper bound {} to data type {}",
                        upperBound,datatype);
                    convertingError = new StringBuilder();
                    convertingError.append("Unable to convert the parsed upper bound value ")
                        .append(upperBound).append(" to data type ").append(datatype);
                } else { //set the converted upper bound
                    upperBound = convertedUpperBound;
                }
            }
            if(lowerBound != null){
                Object convertedLowerBound = converterFactory.convert(lowerBound, datatype, valueFactory);
                if(convertedLowerBound == null){
                    log.warn("Unable to convert lower bound {} to data type {}",
                        lowerBound,datatype);
                    if(convertingError == null){
                        convertingError = new StringBuilder();
                    } else {
                        convertingError.append('\n');
                    }
                    convertingError.append("Unable to convert the parsed value ")
                        .append(lowerBound).append(" to data type ").append(datatype);
                } else { //set the converted lower bound
                    lowerBound = convertedLowerBound;
                }
            }
            if(convertingError != null){ //if there was an error throw an exception
                convertingError.append("Parsed Constraint: \n");
                convertingError.append(jConstraint.toString(4));
                throw new IllegalArgumentException(convertingError.toString());
            }
        }
        if(upperBound == null && lowerBound == null){
            log.warn("Range Constraint does not define an 'upperBound' nor an 'lowerBound'! " +
            		"At least one of the two MUST BE parsed for a valid RangeConstraint.");
            StringBuilder message = new StringBuilder();
            message.append("Range Constraint does not define an 'upperBound' nor an 'lowerBound'!");
            message.append(" At least one of the two MUST BE parsed for a valid RangeConstraint.\n");
            message.append("Parsed Constraint: \n");
            message.append(jConstraint.toString(4));
            throw new IllegalArgumentException(message.toString());
        } else {
            constraint = new RangeConstraint(lowerBound, upperBound, inclusive);
        }
        return constraint;
    }

    /**
     * @param jConstraint
     * @return
     * @throws JSONException
     */
    private static Constraint parseTextConstraint(JSONObject jConstraint) throws JSONException {
        Constraint constraint;
        boolean caseSensitive = jConstraint.optBoolean("caseSensitive", false);
        //parse patternType
        PatternType patternType;
        String jPatternType = jConstraint.optString("patternType",null);
        if(jPatternType == null){
            patternType = PatternType.none;
        } else {
            try {
                patternType = PatternType.valueOf(jPatternType);
            } catch (IllegalArgumentException e) {
                log.warn("Encountered unknown patternType for TextConstraint!",e);
                patternType = PatternType.none;
                StringBuilder message = new StringBuilder();
                message.append("Illegal value for field 'patternType'.\n");
                message.append("Supported values are: ");
                message.append(Arrays.toString(PatternType.values()));
                message.append('\n');
                message.append("Parsed Constraint: \n");
                message.append(jConstraint.toString(4));
                throw new IllegalArgumentException(message.toString());
            }
        }
        //parse languages
        Collection<String> languages;
        String languageKey = null; //support both "languages" and "language"
        if(jConstraint.has("language")){
            languageKey = "language";
        } else if(jConstraint.has("languages")){
            log.warn("The key \"languages\" is deprecated. Use \"language\" instead.");
            languageKey = "languages";
        }
        if(languageKey != null){
            JSONArray jLanguages = jConstraint.optJSONArray(languageKey);
            if(jLanguages != null && jLanguages.length()>0){
                languages = new ArrayList<String>(jLanguages.length());
                for(int i=0;i<jLanguages.length();i++){
                    String lang = jLanguages.getString(i);
                    if(lang != null && !lang.isEmpty()){
                        languages.add(lang);
                    } else if(!languages.contains(null)){
                        languages.add(null);
                    }
                }
                if(languages.isEmpty()){
                    languages = null; //if no one was successfully added set the list back to null
                }
            } else {
                String language = jConstraint.getString(languageKey);
                if(language.isEmpty()){
                    languages = null;
                } else {  //add the single language
                    languages = Collections.singletonList(language);
                }
            }
        } else {
            languages = null;
        }
        //parse text and create constraint
        if(jConstraint.has("text") && !jConstraint.isNull("text")){
            List<String> textConstraints;
            JSONArray jTextConstraints = jConstraint.optJSONArray("text");
            if(jTextConstraints != null){
                textConstraints = new ArrayList<String>(jTextConstraints.length());
                for(int i=0;i<jTextConstraints.length();i++){
                    String text = jTextConstraints.getString(i);
                    if(text != null && !text.isEmpty()){
                        textConstraints.add(jTextConstraints.getString(i));
                    }
                }
            } else {
                String text = jConstraint.getString("text");
                if(text == null || text.isEmpty()){
                    textConstraints = Collections.emptyList();
                } else {
                    textConstraints = Collections.singletonList(text);
                }
            }
            if(textConstraints.isEmpty()){
                StringBuilder message = new StringBuilder();
                message.append("Parsed TextConstraint doese not define a valid (none empty) value for the 'text' property !\n");
                message.append("Parsed Constraint: \n");
                message.append(jConstraint.toString(4));
                throw new IllegalArgumentException(message.toString());
            }
            constraint = new TextConstraint(textConstraints,
                patternType,caseSensitive,
                languages == null?null:languages.toArray(new String[languages.size()]));
        } else {
            StringBuilder message = new StringBuilder();
            message.append("Parsed TextConstraint doese not define the required field 'text'!\n");
            message.append("Parsed Constraint: \n");
            message.append(jConstraint.toString(4));
            throw new IllegalArgumentException(message.toString());
        }
        return constraint;
    }

    /**
     * @param jConstraint
     * @return
     * @throws JSONException
     */
    private static Constraint parseValueConstraint(JSONObject jConstraint, NamespacePrefixService nsPrefixService) throws JSONException {
        Collection<String> dataTypes = parseDatatypeProperty(jConstraint, nsPrefixService);
        final List<Object> valueList;
        if(jConstraint.has("value") && !jConstraint.isNull("value")){
            Object value = jConstraint.get("value");
            if(value instanceof JSONArray){
                valueList = new ArrayList<Object>(((JSONArray)value).length());
                for(int i=0;i<((JSONArray)value).length();i++){
                    Object v = ((JSONArray)value).get(i);
                    if(v == null || v instanceof JSONArray || v instanceof JSONObject){
                        log.warn("Parsed ValueConstraint does define illegal values (values={})!",value);
                        StringBuilder message = new StringBuilder();
                        message.append("Parsed ValueConstraint does define illegal values for field 'value'" +
                        		"(value MUST NOT contain NULL, JSONObject nor JSONArray values)!\n");
                        message.append("Parsed Constraint: \n");
                        message.append(jConstraint.toString(4));
                        throw new IllegalArgumentException(message.toString());
                    }
                    valueList.add(v);
                }
            } else if(value instanceof JSONObject){
                log.warn("Parsed ValueConstraint does define illegal values (values={})!",value);
                StringBuilder message = new StringBuilder();
                message.append("Parsed ValueConstraint does define illegal value for field 'value'" +
                        "(value MUST NOT be an JSON object. Only values and JSONArray to parse" +
                        "multiple values are allowed)!\n");
                message.append("Parsed Constraint: \n");
                message.append(jConstraint.toString(4));
                throw new IllegalArgumentException(message.toString());
            } else {
                valueList = Collections.singletonList(jConstraint.get("value"));
            }
        } else {
            log.warn("Parsed ValueConstraint does not define the required field \"value\"!");
            StringBuilder message = new StringBuilder();
            message.append("Parsed ValueConstraint does not define the required field 'value'!\n");
            message.append("Parsed Constraint: \n");
            message.append(jConstraint.toString(4));
            throw new IllegalArgumentException(message.toString());
        }
        MODE mode = parseConstraintValueMode(jConstraint);
        return new ValueConstraint(valueList,dataTypes,mode);
    }

    /**
     * Parses the {@link MODE} for {@link ValueConstraint}s and 
     * {@link ReferenceConstraint}s, by evaluating the 'mode' attribute of
     * the parsed {@link JSONObject}
     * @param jConstraint the JSON formatted constraint
     * @return the parsed {@link MODE} or <code>null</code> if the 'mode'
     * attribute is not present
     * @throws JSONException if the value of the 'mode' is not an element of the
     * {@link MODE} enumeration.
     */
    private static MODE parseConstraintValueMode(JSONObject jConstraint) throws JSONException {
        MODE mode; 
        if(jConstraint.has("mode")){
            String jmode = jConstraint.getString("mode");
            try {
                mode = MODE.valueOf(jmode);
            } catch (IllegalArgumentException e) {
                String message = String.format("Parsed ValueConstraint defines an " +
                		"unknown MODE %s (supported: %s)!", jmode,
                		Arrays.asList(MODE.values()));
                log.warn(message,e);
                StringBuilder errorMessage = new StringBuilder();
                errorMessage.append(message).append('\n');
                errorMessage.append("Parsed Constraint: \n");
                errorMessage.append(jConstraint.toString(4));
                throw new IllegalArgumentException(message.toString(),e);
            }
        } else {
            mode = null;
        }
        return mode;
    }

    /**
     * @param jConstraint
     * @return
     * @throws JSONException
     */
    private static Collection<String> parseDatatypeProperty(JSONObject jConstraint,NamespacePrefixService nsPrefixService) throws JSONException {
        Collection<String> dataTypes;
        String dataTypeKey = null;
        if(jConstraint.has("datatype")){
            dataTypeKey = "datatype";
        } else if(jConstraint.has("dataTypes")){
            log.warn("The use of \"dataTypes\" is deprecated. Please use \"dataType\" instead");
            dataTypeKey = "dataTypes";
        }
        if(dataTypeKey != null){
            JSONArray jDataTypes = jConstraint.optJSONArray(dataTypeKey);
            if(jDataTypes != null && jDataTypes.length()>0){
                dataTypes = new ArrayList<String>(jDataTypes.length());
                for(int i=0;i<jDataTypes.length();i++){
                    String dataType = jDataTypes.getString(i);
                    //convert prefix:localName to fill URI
                    dataType = dataType != null ? nsPrefixService.getFullName(dataType) : null;
                    if(dataType != null && !dataType.isEmpty()){
                        dataTypes.add(dataType);
                    }
                }
                if(dataTypes.isEmpty()){
                    dataTypes = null; //if no one was successfully added set the list back to null
                }
            } else {
                String dataType = jConstraint.getString(dataTypeKey);
                //convert prefix:localName to fill URI
                dataType = dataType != null ? nsPrefixService.getFullName(dataType) : null;
                if(dataType != null && !dataType.isEmpty()){
                    dataTypes = Collections.singleton(dataType);
                } else {
                    dataTypes = null;
                }
            }
        } else {
            dataTypes = null;
        }
        return dataTypes;
    }

    /**
     * @param jConstraint
     * @return
     * @throws JSONException
     */
    private static Constraint parseReferenceConstraint(JSONObject jConstraint, NamespacePrefixService nsPrefixService) throws JSONException {
        final List<String> refList;
        if(jConstraint.has("value") && !jConstraint.isNull("value")){
            Object value = jConstraint.get("value");
            if(value instanceof JSONArray){
                refList = new ArrayList<String>(((JSONArray)value).length());
                for(int i=0;i<((JSONArray)value).length();i++){
                    String field = ((JSONArray)value).getString(i);
                    field = field != null ? nsPrefixService.getFullName(field) : null;
                    if(field != null && !field.isEmpty()){
                        refList.add(field);
                    }
                }
            } else if(value instanceof JSONObject){
                log.warn("Parsed ValueConstraint does define illegal values (values={})!",value);
                StringBuilder message = new StringBuilder();
                message.append("Parsed ValueConstraint does define illegal value for field 'value'" +
                        "(value MUST NOT be an JSON object. Only values and JSONArray to parse" +
                        "multiple values are allowed)!\n");
                message.append("Parsed Constraint: \n");
                message.append(jConstraint.toString(4));
                throw new IllegalArgumentException(message.toString());
            } else {
                String field = jConstraint.getString("value");
                field = field != null ? nsPrefixService.getFullName(field) : null;
                if(field != null){
                    refList = Collections.singletonList(field);
                } else {
                    refList = Collections.emptyList();
                }
            }
            if(refList.isEmpty()){
                log.warn("Parsed ReferenceConstraint does not define a single valid \"value\"!");
                StringBuilder message = new StringBuilder();
                message.append("Parsed ReferenceConstraint does not define a single valid 'value'!\n");
                message.append("This means values where only null, empty string or '{prefix}:{localname}' values with unknown {prefix}\n");
                message.append("Parsed Constraint: \n");
                message.append(jConstraint.toString(4));
                throw new IllegalArgumentException(message.toString());                
            }
            MODE mode = parseConstraintValueMode(jConstraint);
            return new ReferenceConstraint(refList,mode);
        } else {
            log.warn("Parsed ReferenceConstraint does not define the required field \"value\"!");
            StringBuilder message = new StringBuilder();
            message.append("Parsed ReferenceConstraint does not define the required field 'value'!\n");
            message.append("Parsed Constraint: \n");
            message.append(jConstraint.toString(4));
            throw new IllegalArgumentException(message.toString());
        }
    }
}
