public class PageAnnoCount {
	protected int _count = 0;
    // These three properties should really be a canvas object...
    protected Canvas _canvas = null;
	protected String _pageId = "";
    protected String _label = "";
    protected String _shortId = "";
    protected Manifest _manifest = null;

	public PageAnnoCount(final Canvas pCanvas, final int pCount, final Manifest pManifest) {
		this.setCanvas(pCanvas);
		this.setCount(pCount);
        this.setManifest(pManifest);
	}

    public Manifest getManifest() {
        return _manifest;
    }

    public void setManifest(final Manifest pManifest) {
        _manifest = pManifest;
    }

	
	/**
	 * Get count.
	 *
	 * @return count as int.
	 */
	public int getCount() {
	    return _count;
	}
	
	/**
	 * Set count.
	 *
	 * @param count the value to set.
	 */
	public void setCount(final int pCount) {
	     _count = pCount;
	}
	
    /**
     * Get canvas.
     *
     * @return canvas as Canvas.
     */
    public Canvas getCanvas() {
        return _canvas;
    }
    
    /**
     * Set canvas.
     *
     * @param canvas the value to set.
     */
    public void setCanvas(final Canvas pCanvas) {
         _canvas = pCanvas;
    }

	public String toString() {
		return "Canvas=>" + _canvas.toString() + "\tCount=>" + _count;
	}
} public class Manifest {
	protected String _URI = "";
	protected String _shortId = "";
	protected String _label = "";
    protected Map<String, Object> _json = null;
    protected List<Canvas> _canvases = new ArrayList<Canvas>();


	public Manifest() {
	}

    /*
     * Set manifest before it has a short id
     */
     public Manifest(final Map<String, Object> pJson) throws IOException {
        this(pJson, "");
    }

    public Manifest(final Map<String, Object> pJson, final String pShortId) throws IOException {
        this.setJson(pJson);
        if (!this.getType().equals("sc:Manifest")) {
            throw new IOException("Can't create manifest as type was incorrect. Expected sc:Manifest but got: " + this.getType());
        }
        if (this.getCanvases().isEmpty()) {
            throw new IOException("Can't load manifest as it has no pages.");
        }
        this.setShortId(pShortId);
        this.setURI((String)pJson.get("@id"));
    }

    public Map<String, Object> toJson() {
        return _json;
    }

    public void setJson(final Map<String, Object> pJson) {
        _json = pJson;
        this.setURI((String)_json.get("@id"));
        this.setLabel((String)_json.get("label")); // will fail if there is a multilingual string

        Map<String,Object> tSequence = null;
        if (_json.get("sequences") instanceof List ) {
            if (!((List)_json.get("sequences")).isEmpty()) {
                tSequence = ((List<Map<String, Object>>)_json.get("sequences")).get(0);
            }
        } else {
            tSequence = (Map<String, Object>)_json.get("sequences");
        }

        _canvases = new ArrayList<Canvas>();
        if (tSequence != null) {
            for (Map<String, Object> tCanvas : (List<Map<String, Object>>)tSequence.get("canvases")) {
                _canvases.add(new Canvas((String)tCanvas.get("@id"), (String)tCanvas.get("label")));
            }
        }
    }

    public String getType() {
        try {
            return (String)getJson().get("@type");
        } catch (IOException tExcpt) {
            tExcpt.printStackTrace();
            return null;
        }
    }

    public Canvas getCanvas(final String pId) {
        for (Canvas tCanvas : _canvases) {
            if (tCanvas.getId().equals(pId)) {
                return tCanvas;
            }
        }
        return null;
    }

    /**
     *
     */
    public Map<String,Object> getJson() throws IOException {
        if (_json == null) {
            this.setJson((Map<String,Object>)JsonUtils.fromInputStream(new URL(_URI).openStream()));
        }
        return _json;
    }    
	
	/**
	 * Get URI.
	 *
	 * @return URI as String.
	 */
	public String getURI() {
	    return _URI;
	}
	
	/**
	 * Set URI.
	 *
	 * @param URI the value to set.
	 */
	public void setURI(final String pURI) {
	     _URI = pURI;
	}
	
	/**
	 * Get shortId.
	 *
	 * @return shortId as String.
	 */
	public String getShortId() {
        if (_shortId == null || _shortId.isEmpty()) {
            if (_URI.endsWith("manifest.json")) {
                String[] tURI = _URI.split("/");
                _shortId = tURI[tURI.length - 2];
            } else {
                try {
                    _shortId = AnnotationUtils.getHash(_URI, "md5");
                } catch (IOException tExcpt) {
                    tExcpt.printStackTrace();
                }
            }
        }
	    return _shortId;
	}
	
	/**
	 * Set shortId.
	 *
	 * @param shortId the value to set.
	 */
	public void setShortId(final String pShortId) {
	     _shortId = pShortId;
	}
	
	/**
	 * Get label.
	 *
	 * @return label as String.
	 */
	public String getLabel() {
	    return _label;
	}
	
	/**
	 * Set label.
	 *
	 * @param label the value to set.
	 */
	public void setLabel(final String pLabel) {
	     _label = pLabel;
	}

    
    public List<Canvas> getCanvases() {
        return _canvases;
    }

    public boolean equals(Object pOther) {
        if (pOther instanceof Manifest) {
            return _URI.equals(((Manifest)pOther).getURI());
        } else {
            return false;
        }
    }

    public String toString() {
        return "Id: " + _URI + "\nShortId: " + _shortId + "\nLabel: " + _label + "\nCanvases: " + _canvases.size();
    }
} public class Canvas {
    protected String _id = "";
    protected String _label = "";
    protected String _shortId = "";

    public Canvas(final String pId, final String pLabel) {
        this.setId(pId);
        this.setLabel(pLabel);
    }

    public Canvas(final Map<String, Object> pJson) {
        if (pJson.get("@id") != null) {
            _id = (String)pJson.get("@id");
        }
        if (pJson.get("label") != null) {
            _label = (String)pJson.get("label");
        }
        if (pJson.get("http://purl.org/dc/terms/identifier") != null) {
            _shortId = (String)pJson.get("http://purl.org/dc/terms/identifier");
        }
    }

    public Map<String, Object> toJson() {
        Map<String, Object> tJson = new HashMap<String,Object>();
        tJson.put("@id", _id);
        tJson.put("@type", "sc:Canvas");
        if (_label != null && !_label.trim().isEmpty()) {
            tJson.put("label", _label);
        }
        if (_shortId != null && !_shortId.trim().isEmpty()) {
            tJson.put("http://purl.org/dc/terms/identifier", _shortId);
        }
        return tJson;
    }
    
    /**
     * Get uri.
     *
     * @return uri as String.
     */
    public String getId() {
        return _id;
    }
    
    /**
     * Set uri.
     *
     * @param uri the value to set.
     */
    public void setId(final String pURI) {
         _id = pURI;
    }
    
    /**
     * Get label.
     *
     * @return label as String.
     */
    public String getLabel() {
        return _label;
    }
    
    /**
     * Set label.
     *
     * @param label the value to set.
     */
    public void setLabel(final String pLabel) {
         _label = pLabel;
    }
    
    protected static Map<String,String> _canvasIdMap = new HashMap<String, String>();
    /**
     * Get shortId.
     *
     * @return shortId as String.
     */
    public String getShortId() {
        if (_shortId == null || _shortId.trim().isEmpty()) {
            if (_canvasIdMap.get(_id) == null) {
                try {
                    _shortId = AnnotationUtils.getHash(_id, "md5");
                    // Cache
                    _canvasIdMap.put(_id, _shortId);
                } catch (IOException tExcpt) {
                    tExcpt.printStackTrace();
                    return null;
                }
            } else {
                // Use cached
                _shortId = _canvasIdMap.get(_id);
            }
        }
        return _shortId;
    }
    
    /**
     * Set shortId.
     *
     * @param shortId the value to set.
     */
    public void setShortId(final String pShortId) {
         _shortId = pShortId;
    }

    public String toString() {
        return "Canvas:\n\tid: " + _id + "\n\tShort id: " + _shortId + "\n\tLabel: " + _label; 
    }

    public boolean equals(final Object pOther) {  
        return _id.equals(((Canvas)pOther).getId());
    }
} public class Target {
    protected Map<String, Object> _json = null;

    public Target(final Object pObj) {
        if (pObj instanceof String) {
            init((String)pObj);
        } else {
            init((Map<String, Object>)pObj);
        }
    }

    protected void init(final String pTarget) {
        _json = new HashMap<String, Object>();
        if (pTarget.indexOf("#") != -1) {
            // Instance of FragementSelector
            String[] tOnStr = pTarget.split("#");

            _json = new HashMap<String,Object>();
            _json.put("@type", "oa:SpecificResource");
            _json.put("full", tOnStr[0]);

            Map<String,Object> tSelector = new HashMap<String,Object>();
            _json.put("selector", tSelector);
            tSelector.put("@type", "oa:FragmentSelector");
            tSelector.put("value", tOnStr[1]);
        } else {
            // Plain URI
            _json.put("@id", pTarget);
        }
    }

    /** 
     * Returns null if no region
     */ 
    public String getRegion() {
        String tRegion = null;
        if (_json.get("@type") != null && _json.get("@type").equals("oa:SpecificResource") && _json.get("selector") != null) {
            Map<String,Object> tSelector = (Map<String,Object>)_json.get("selector");
            if (tSelector.get("@type") != null && tSelector.get("@type").equals("oa:FragmentSelector") && tSelector.get("value") != null) {
                tRegion = (String)tSelector.get("value");
            }
        }
        return tRegion;
    }

    protected void init(final Map<String, Object> pTarget) {
        _json = pTarget;
    }

    public Canvas getCanvas() {
        String tKey = "";
        if (_json.get("full") != null) {
            tKey = "full";
        } else if(_json.get("source") != null) {
            tKey = "source";
        } else {
            return null; // No linked canvas
        }
        String tId = "";
        String tLabel = "";
        if (_json.get(tKey) instanceof Map) {
            Map<String,Object> tCanvasMap = (Map<String,Object>)_json.get(tKey);
            tId = (String)tCanvasMap.get("@id");
            if (tCanvasMap.get("label") != null) {
                tLabel = (String)tCanvasMap.get("label");
            }
        } else {
            tId = (String)_json.get(tKey);
        }
        return new Canvas(tId, tLabel);
    }

    public Manifest getManifest() {
        Manifest tManifest = null;
        if (_json.get("within") != null) {
            tManifest = new Manifest();
            if (_json.get("within") instanceof Map) {
                tManifest.setURI((String)((Map<String,Object>)_json.get("within")).get("@id"));
                if (((Map<String,Object>)_json.get("within")).get("label") != null) {
                    tManifest.setLabel((String)((Map<String,Object>)_json.get("within")).get("label"));
                }
            } else {
                tManifest.setURI((String)_json.get("within"));
            }
        }
        return tManifest;
    }

    public void setManifest(final Manifest pManifest) {
        if (!pManifest.getLabel().isEmpty()) {
            // Create map
            Map<String, String> tWithin = new HashMap<String, String>();
            tWithin.put("@id", pManifest.getURI());
            tWithin.put("@type", "sc:Manifest");
            tWithin.put("label", pManifest.getLabel());

            _json.put("within", tWithin);
        } else {
            _json.put("within", pManifest.getURI());
        }
    }

    public Map<String, Object> toJson() {
        return _json;
    }
} public class Annotation {
    public static final String FULL_TEXT_PROPERTY = "http://dev.llgc.org.uk/sas/full_text";
    protected static Logger _logger = LogManager.getLogger(Annotation.class.getName());
	protected SimpleDateFormat _dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
	protected Encoder _encoder = null;
    protected Map<String,Object> _annotation = null;
    protected List<Body> _bodies = null;
    protected List<Target> _targets = null;

    public Annotation(final Map<String,Object> pAnno) {
        this(pAnno, null);
    }

    public Annotation(final Map<String,Object> pAnno, final Encoder pEncoder) {
        if (pEncoder == null) {
            try {
                _encoder = StoreConfig.getConfig().getEncoder();
            } catch (ServletException tExcpt) {
                System.err.println("Failed to load encoder from config");
                tExcpt.printStackTrace();
            }
        } else {
            _encoder = pEncoder;
        }
        this.setJson(pAnno);
    }

    public void setEncoder(final Encoder pEncoder) {
        _encoder = pEncoder;
    }

    public void setJson(final Map<String, Object> pJson) {
        _annotation = pJson;
        this.init();
    }

    protected void init() {
        this.standaiseAnno();
        this.expandTarget();
        addMetadata();
        if (_encoder != null) {
            _encoder.encode(_annotation);
        }

        _bodies = new ArrayList<Body>();
        if (_annotation.get("resource") != null) {
            if (_annotation.get("resource") instanceof List) {
                for (Object tBody : (List<Object>)_annotation.get("resource")) {
                    _bodies.add(new Body(tBody));
                }
            } else {
                _bodies.add(new Body(_annotation.get("resource")));
            }
        }

        _targets = new ArrayList<Target>();
        if (_annotation.get("on") != null) {
            if (_annotation.get("on") instanceof List) {
                for (Object tTarget : (List<Object>)_annotation.get("on")) {
                    _targets.add(new Target(tTarget));
                }
            } else {
                _targets.add(new Target(_annotation.get("on")));
            }
        }
    }

    protected void standaiseAnno() {
        // Compact created
        if (_annotation.get("http://purl.org/dc/terms/created") != null) {
            _annotation.put("dcterms:created", _annotation.get("http://purl.org/dc/terms/created"));
            _annotation.remove("http://purl.org/dc/terms/created");
        }
        if (_annotation.get("http://purl.org/dc/terms/modified") != null) {
            _annotation.put("dcterms:modified", _annotation.get("http://purl.org/dc/terms/modified"));
            _annotation.remove("http://purl.org/dc/terms/modified");
        }
        if (_annotation.get("resource") != null && _annotation.get("resource") instanceof Map) {
            // turn into Array?
            List tList = new ArrayList();
            tList.add(_annotation.get("resource"));
            
            _annotation.put("resource", tList);
        }
    }

    public String getId() {
        return (String)_annotation.get("@id");
    }

    public void setId(final String pId) {
        _annotation.put("@id", pId);
    }

    // In search api this is the snippet
    public void setLabel(final String pLabel) {
        _annotation.put("label", pLabel);
    }

    public String getType() {
        return (String)_annotation.get("@type");
    }

    public void setType(final String pType) {
        _annotation.put("@type", pType);
    }

    public List<String> getMotivations() {
        List<String> tMotivations = new ArrayList<String>();
        if (_annotation.get("motivation") instanceof String) {
            tMotivations.add((String)_annotation.get("motivation"));
        } else {
            tMotivations = (List<String>)_annotation.get("motivation");
        }
        return tMotivations;
    }

    public void setMotivations(final List<String> pMotivations) {
        _annotation.put("motivation", pMotivations);
    }

    public void setCreated(final String pDate) {
        if (_annotation.get("dcterms:" + DCTerms.created.getLocalName()) == null) {
            _annotation.put("dcterms:" + DCTerms.created.getLocalName(), pDate);
        }
    }

    public void setCreated(final Date pDate) {
        _annotation.put("dcterms:" + DCTerms.created.getLocalName(), _dateFormatter.format(pDate));
    }

    public Date getCreated() {
        if (_annotation.get("dcterms:" + DCTerms.created.getLocalName()) == null) {
            return null;
        } else {
            String tDate = (String)_annotation.get("dcterms:" + DCTerms.created.getLocalName());
            try {
                return _dateFormatter.parse(tDate);
            } catch (ParseException tExcpt) {
                // This shouldn't happen as date is created above...
                tExcpt.printStackTrace();
                return null;
            }
        }
    }

    public Date getModified() {
        if (_annotation.get("dcterms:" + DCTerms.modified.getLocalName()) == null) {
            return null;
        } else {
            if (_annotation.get("dcterms:" + DCTerms.modified.getLocalName()) instanceof List) {
                // invalid really so just reset
                this.updateModified();
            }
            String tDate = (String)_annotation.get("dcterms:" + DCTerms.modified.getLocalName());
            try {
                return _dateFormatter.parse(tDate);
            } catch (ParseException tExcpt) {
                // This shouldn't happen as date is created above...
                tExcpt.printStackTrace();
                return null;
            }
        }
    }

    public void updateModified() {
		_annotation.put("dcterms:" + DCTerms.modified.getLocalName(), _dateFormatter.format(new Date()));
    }

    public void addWithin(final Manifest pManifest, final Canvas pCanvas) {
        for (Target tTarget : this.getTargets()) {
            if (tTarget.getCanvas().equals(pCanvas)) {
                tTarget.setManifest(pManifest);
            }
        }
    }

    public List<Body> getBodies() {
        return _bodies; 
    }

    public List<Target> getTargets() {
        return _targets; 
    }

    /**
     * Check if the load annotaiton is valid
     */
    public String checkValid() throws MalformedAnnotation {
        try {
            URI tURI = new URI(this.getId()); // Check if this is a valid URI otherwise it will fail to load correctly.
            if (!tURI.isAbsolute()) {
                // No scheme so invalid
                throw new MalformedAnnotation("URI: '" + tURI + "' doesn't contain a scheme");
            }
        } catch (URISyntaxException tExcpt) {
            throw new MalformedAnnotation("URI: " + this.getId() + " is invalid due to " + tExcpt.getMessage());
        }

        if (_annotation.get("on") == null) {
            throw new MalformedAnnotation("Missing on");
        }
        StringBuffer tOutput = new StringBuffer();
        boolean tMalformed = false;
        for (Target tTarget : this.getTargets()) {
            // Look to see if selector value is an array. It should be a string.
            Map<String, Object> tOn = tTarget.toJson();
            if (tOn.get("selector") != null
                    && tOn.get("selector") instanceof Map
                    && ((Map<String,Object>)tOn.get("selector")).get("value") != null
                    && ((Map<String,Object>)tOn.get("selector")).get("value") instanceof List) {
                tOutput.append("Annotation " + this.getId() + " has an array in the on/selector/value and this should be a string");
                tMalformed = true;
            }
        }
        if (tMalformed) {
            throw new MalformedAnnotation(tOutput.toString());
        } else {
            // in case of non critical errors:
            return tOutput.toString();
        }
    }

    public List<Target> getMissingWithin() {
        List<Target> tMissing = new ArrayList<Target>();

        for (Target tTarget : this.getTargets()) {
            if (tTarget.getManifest() == null) {
                tMissing.add(tTarget);
            }
        }
		return tMissing;
    }

    protected void addMetadata() {
		// Add create date if it doesn't already have one
		if (this.getCreated() == null) {
			this.setCreated(new Date());
            this.updateModified();
		}
	}

    protected void expandTarget() {
		String tURI = null;
		Map<String,Object> tSpecificResource = null;
		if (_annotation.get("on") instanceof String) {
			tURI = (String)_annotation.get("on");
			tSpecificResource = this.expandOn(tURI);
            _annotation.put("on", tSpecificResource);
        } else if (_annotation.get("on") instanceof List) {
            List <Object> tOnList = new ArrayList<Object>();
            for (Object tOn : (List<Object>)_annotation.get("on")) {
                if (tOn instanceof String) {
                    tOnList.add(expandOn((String)tOn));
                } else {
                    tOnList.add(tOn);
                }
            }
            _annotation.put("on", tOnList);
		} else if (_annotation.get("on") instanceof Map) {
			tSpecificResource = (Map<String,Object>)_annotation.get("on");

			if (tSpecificResource.get("@id") == null || ((String)tSpecificResource.get("@id")).indexOf("#") == -1) {
				return; // No id to split or no fragement
			}
			if (tSpecificResource.get("selector") != null) {
				return; // already have a selector
			}
			tURI = (String)tSpecificResource.get("@id");

            Map<String,Object> tNewSpecificResource = this.expandOn(tURI);
            if (tSpecificResource.get("within") != null) {
                tNewSpecificResource.put("within", tSpecificResource.get("within"));
            }
            _annotation.put("on", tNewSpecificResource);
		}
        // Unrecognised annotation
	}

    protected Map<String, Object> expandOn(final String tOn) {
        if (tOn.contains("#")) {
            String[] tOnStr = tOn.split("#");

            Map<String,Object> tSpecificResource = new HashMap<String,Object>();
            tSpecificResource.put("@type", "oa:SpecificResource");
            tSpecificResource.put("full", tOnStr[0]);

            Map<String,Object> tSelector = new HashMap<String,Object>();
            tSpecificResource.put("selector", tSelector);
            tSelector.put("@type", "oa:FragmentSelector");
            tSelector.put("value", tOnStr[1]);

            return tSpecificResource;
        } else {
            return null; // throw execption?
        }
    }

    private List<Map<String,Object>> map2list(final Map<String,Object> tMap) {
        List<Map<String, Object>> tNewList = new ArrayList<Map<String,Object>>();
        tNewList.add(tMap);
        return tNewList;
    }

    public Map<String,Object> toJson() {
        if (_encoder != null) {
            Map<String,Object> tClone = (Map<String,Object>)((HashMap)_annotation).clone();
            _encoder.decode(tClone);

            return tClone;
        } else {    
            return _annotation;
        }
    }

    public Map<String,Object> jsonTargetRect() {
        Map<String,Object> tClone = (Map<String,Object>)((HashMap)_annotation).clone();
        collapseFragment(tClone);
        if (((List)tClone.get("resource")).size() == 1) {
            tClone.put("resource", ((List)tClone.get("resource")).get(0)); 
        }
        return tClone;
    }

    // Need to move fragement into on
	public void collapseFragment(final Map<String,Object> pAnnotationJson) {
		if (pAnnotationJson.get("on") instanceof Map) {
            collapseFragmentOn(pAnnotationJson, (Map<String,Object>)pAnnotationJson.get("on"));
		} else if (pAnnotationJson.get("on") instanceof List) {
            for (Map<String,Object> tOn : (List<Map<String,Object>>)pAnnotationJson.get("on")) {
                collapseFragmentOn(pAnnotationJson, tOn);
            }
		}	// otherwise its already collapsed as its a string
	}
	public void collapseFragmentOn(final Map<String,Object> pAnnotationJson, final Map<String,Object> pOn) {
		if (pOn.get("selector") != null) {
			try {
                String tFragement = "";
                if (((Map)pOn.get("selector")).get("value") != null) {
    				tFragement = (String)((Map)pOn.get("selector")).get("value");
                } else {
    				tFragement = (String)((Map)((Map)pOn.get("selector")).get("default")).get("value");
                }
				String tTarget = (String)pOn.get("full");
				pAnnotationJson.put("on", tTarget + "#" + tFragement);
			} catch (ClassCastException tExcpt) {
				System.err.println("Failed to transform annotation");
				try {
					System.out.println(JsonUtils.toPrettyString(pAnnotationJson));
				} catch (IOException	tIOExcpt) {
					System.out.println("Failed to print failing annotation " + tIOExcpt);
				}
				throw tExcpt;
			}
		}
	}


    public String toString() {
        try {
            return JsonUtils.toPrettyString(_annotation);
        } catch (IOException tExcpt) {
            return "Failed to pretty print json due to " + tExcpt.getMessage();
        }
    }
} public class AnnotationList {
    protected List<Annotation> _annotations = null;
    protected String _id = "";

    public AnnotationList() {
        _annotations = new ArrayList<Annotation>();
    }

    public AnnotationList(final List<Map<String, Object>> pAnnos) {
        _annotations = new ArrayList<Annotation>();
        for (Map<String,Object> tAnno : pAnnos) {
            _annotations.add(new Annotation(tAnno));
        }
    }

    public AnnotationList(final List<Model> pAnnotations, final boolean pCompact) throws IOException {
        _annotations = new ArrayList<Annotation>();
        AnnotationUtils tUtils = new AnnotationUtils();
        for (Model tModel : pAnnotations) {
            _annotations.add(new Annotation(tUtils.frameAnnotation(tModel, pCompact)));
        }
    }
    
    /**
     * Get id.
     *
     * @return id as String.
     */
    public String getId() {
        return _id;
    }
    
    /**
     * Set id.
     *
     * @param id the value to set.
     */
    public void setId(final String pId) {
         _id = pId;
    }

    public void add(final Annotation pAnno) {
        _annotations.add(pAnno);
    }
    
    public Annotation get(final String pAnnoId) {
        for (Annotation tAnno : _annotations) {
            if (tAnno.getId().equals(pAnnoId)) {
                return tAnno;
            }
        }
        return null;
    }

    public int size() {
        return _annotations.size();
    }

    public List<Annotation> getAnnotations() {
        return _annotations;
    }
    
    public void setAnnotations(final List<Annotation> pAnnos) {
        _annotations = pAnnos;
    }

    public Map<String, Object> toJson() throws IOException {
        Map<String,Object> tJson = new HashMap<String,Object>();
        tJson.put("@context", "http://iiif.io/api/presentation/2/context.json");
        tJson.put("@id", _id);
        tJson.put("@type", "sc:AnnotationList");

        List<Map<String,Object>> tAnnos = new ArrayList<Map<String,Object>>();
        tJson.put("resources", tAnnos);

        for (Annotation tAnno: _annotations) {
            tAnnos.add(tAnno.toJson());
        }

        return tJson;
    }
    
} public class IIIFSearchResults extends AnnotationList {
    protected AnnoListNav _nav = null;
    protected int _startIndex = -1;
    protected String _next = "";

    public IIIFSearchResults() {
        super();
    }

    public Map<String, Object> toJson() throws IOException {
        Map<String,Object> tJson = new HashMap<String,Object>();
        tJson.put("@context", "http://iiif.io/api/presentation/2/context.json");
        tJson.put("@id", _id);
        tJson.put("@type", "sc:AnnotationList");
        if (_nav != null) {
            tJson.put("within", _nav.toJson());
        }
        if (_next != null && !_next.isEmpty()) {
            tJson.put("next", _next);
        }
        if (_startIndex != -1) {
            tJson.put("startIndex", _startIndex);
        }

        List<Map<String,Object>> tAnnos = new ArrayList<Map<String,Object>>();
        tJson.put("resources", tAnnos);

        for (Annotation tAnno: _annotations) {
            // TODO need to collapse the annotations into #xywh
            tAnnos.add(tAnno.jsonTargetRect());
        }

        return tJson;
    }
    /**
     * Get nav.
     *
     * @return nav as AnnoListNav.
     */
    public AnnoListNav getNav() {
        return _nav;
    }
    
    /**
     * Set nav.
     *
     * @param nav the value to set.
     */
    public void setNav(final AnnoListNav pNav) {
         _nav = pNav;
    }
    
    /**
     * Get startIndex.
     *
     * @return startIndex as protected.
     */
    public int getStartIndex() {
        return _startIndex;
    }
    
    /**
     * Set startIndex.
     *
     * @param startIndex the value to set.
     */
    public void setStartIndex(final int pStartIndex) {
         _startIndex = pStartIndex;
    }
    
    /**
     * Get next.
     *
     * @return next as String.
     */
    public String getNext() {
        return _next;
    }
    
    /**
     * Set next.
     *
     * @param next the value to set.
     */
    public void setNext(final String pNext) {
         _next = pNext;
    }

} public class SearchQuery {
	protected String _query = "";
	protected List<String> _motivations = null;
	protected List<DateRange> _dates = null;
	protected List<String> _users = null;
	protected int _resultsPerPage = 1000;
	protected int _page = 0;
	protected String _scope = "";
	protected String _baseURI = "";

	public SearchQuery(final String pQuery) {
		this.setQuery(pQuery);
	}

	public SearchQuery(final URI pURI) throws ParseException {
		this.setBaseURI(pURI);

		List<NameValuePair> tParamsList = URLEncodedUtils.parse(pURI, Charset.forName("UTF-8"));
		Map<String,String> tParams = new HashMap<String,String>();
		for (NameValuePair tCurrent : tParamsList) {
			tParams.put(tCurrent.getName(), tCurrent.getValue());
		}
		if (tParams.get("q") != null) {
			this.setQuery(tParams.get("q"));
		} 
		if (tParams.get("motivation") != null) {
			this.setQuery(tParams.get("motivation"));
		} 
		if (tParams.get("date") != null) {
			this.setDateRanges(tParams.get("date"));
		} 
		if (tParams.get("user") != null) {
			this.setUsers(tParams.get("user"));
		} 
		if (tParams.get("page") != null) {
			this.setPage(Integer.parseInt(tParams.get("page")));
		} 
	}

	protected String convertListToString(final String pKey, final List pList) {
		if (pList != null && !pList.isEmpty()) {
			StringBuffer tBuff = new StringBuffer("&");
			tBuff.append(pKey);
			tBuff.append("=");
			for (Object tChild: pList) {
				tBuff.append(tChild);
				tBuff.append(" ");
			}
			return tBuff.toString().trim();
		} else {
			return "";
		}
	}

	public URI toURI() throws URISyntaxException {
		return new URI(_baseURI + "?" + this.toQueryString());
	}

	public String toQueryString() {
		StringBuffer tBuff = new StringBuffer("q=");
        try {
            tBuff.append(URLEncoder.encode(_query, "UTF-8"));
            if (_motivations != null) {
                tBuff.append("&");
                tBuff.append(URLEncoder.encode(this.convertListToString("motivation", _motivations), "UTF-8"));
            }
            if (_dates != null) {
                tBuff.append("&");
                tBuff.append(this.convertListToString("date", _dates));
            }	
            if (_users != null) {
                tBuff.append("&");
                tBuff.append(URLEncoder.encode(this.convertListToString("user", _users), "UTF-8"));
            }	
            if (_page != 0) {
                tBuff.append("&");
                tBuff.append("page=" + _page);
            }	
        } catch (UnsupportedEncodingException tExcpt) {
            // shouldn't happen as UTF-8 should be supported.
            tExcpt.printStackTrace();
        }
		return tBuff.toString();
	}

	public void setBaseURI(final URI pURI) {
		StringBuffer tURI = new StringBuffer(pURI.getScheme());
		tURI.append("://");
		tURI.append(pURI.getAuthority());
		tURI.append(pURI.getPath());
		_baseURI = tURI.toString();
	}

	public void setBaseURI(final String pBaseURI) {
		_baseURI = pBaseURI;
	}

	public String getBaseURI() {
		return _baseURI;
	}

	public void setScope(final String pScope) {
		_scope = pScope;
	}

	public String getScope() {
		return _scope;
	}
		
	public void setQuery(final String pQuery) {
		_query = pQuery;
	}

	public String getQuery() {
		return _query;
	}

	public int getResultsPerPage() {
		return _resultsPerPage;
	}

	public void setResultsPerPage(final int pNumResults) {
		_resultsPerPage = pNumResults;
	}

	public void setPage(final int pPage) {
		_page = pPage;
	}

	public int getIndex() {
		return _page * _resultsPerPage;
	}

	public int getPage() {
		return _page;
	}

	// A space separated list of motivation terms. If multiple motivations are supplied, an annotation matches the search if any of the motivations are present.
	public void setMotivations(final String pMotivations) {
		StringTokenizer tTokenizer = new StringTokenizer(pMotivations);
		_motivations = new ArrayList<String>();
		while (tTokenizer.hasMoreTokens()) {
			_motivations.add(tTokenizer.nextToken());
		}
	}

	public List<String> getMotivations() {
		return _motivations;
	}

	// A space separated list of date ranges. An annotation matches if the date on which it was created falls within any of the supplied date ranges. The dates must be supplied in the ISO8601 format: YYYY-MM-DDThh:mm:ssZ/YYYY-MM-DDThh:mm:ssZ. The dates must be expressed in UTC and must be given in the Z based format.
	public void setDateRanges(final String pDates) throws ParseException {
		StringTokenizer tTokenizer = new StringTokenizer(pDates);
		_dates = new ArrayList<DateRange>();
		while (tTokenizer.hasMoreTokens()) {
			_dates.add(new DateRange(tTokenizer.nextToken()));
		}
	}

	public List<DateRange> getDateRanges() {
		return _dates;
	}

	public void setUsers(final String pUsers) {
		StringTokenizer tTokenizer = new StringTokenizer(pUsers);
		_users = new ArrayList<String>();
		while (tTokenizer.hasMoreTokens()) {
			_users.add(tTokenizer.nextToken());
		}
	}

	public List<String> getUsers() {
		return _users;
	}
} public class IDConflictException extends Exception {
	public IDConflictException() {
		super();
	}

	public IDConflictException(final String pMessage) {
		super(pMessage);
	}
} public class MalformedAnnotation extends Exception {
	public MalformedAnnotation() {
		super();
	}

	public MalformedAnnotation(final String pMessage) {
		super(pMessage);
	}
} public class AnnotationUtils {
	protected static Logger _logger = LogManager.getLogger(AnnotationUtils.class.getName());

	protected File _contextDir = null;
	protected Encoder _encoder = null;

    // Called by servlet
    public AnnotationUtils() {
        try {
            _contextDir = StoreConfig.getConfig().getRealPath("/contexts"); // pContextDir;
            _encoder = StoreConfig.getConfig().getEncoder();
        } catch (javax.servlet.ServletException tExcpt) {
            //tExcpt.printStackTrace();
        }
    }

    // Called by tests
	public AnnotationUtils(final File pContextDir, final Encoder pEncoder) {
        _contextDir = pContextDir;
        _encoder = pEncoder;
    }

	/**
	 * Convert a IIIF annotation list into a list of annotations that have fragement
	 * identifiers
	 * @param InputStream the input stream to read to get the IIIF annotation list
	 */
	public List<Map<String,Object>> readAnnotationList(final InputStream pStream, final String pBaseURL) throws IOException {
		Object inputList = JsonUtils.fromInputStream(pStream);
        _logger.debug("Original untouched annotation list:");
        _logger.debug(JsonUtils.toPrettyString(inputList));
        List<Map<String,Object>> tAnnotations = null;
        if (inputList instanceof Map) {
            Map<String,Object> tAnnotationList = (Map<String,Object>)inputList;
            tAnnotations = (List<Map<String,Object>>)tAnnotationList.get("resources");
        } else if (inputList instanceof List) {
            tAnnotations = (List<Map<String,Object>>)inputList;
        } else {
            throw new IOException("Don't recognise annotation list " + inputList.getClass().getName());
        }


		//if (tAnnotationList.get("@id") == null) {
		//	_logger.debug(JsonUtils.toPrettyString(tAnnotationList));
		//	throw new IOException("Annotation list must have a @id at root");
		//}
		//String[] tListURI = ((String)tAnnotationList.get("@id")).split("/");
		//String tBucketId = tListURI[tListURI.length - 1].replaceAll(".json","");
		int tAnnoCount = 0;
		for (Map<String, Object> tAnno : tAnnotations) {
			if (tAnno.get("@id") == null) {

				StringBuffer tBuff = new StringBuffer(pBaseURL);
				tBuff.append(getHash(getTarget(tAnno), "md5"));
				tBuff.append("/");
				tBuff.append(tAnnoCount++);
				tAnno.put("@id", tBuff.toString());
			}
			tAnno.put("@context", this.getContext()); // need to add context to each annotation fixes issue #18

			Map<String, Object> tResource = null;
			if (tAnno.get("resource") instanceof List) {
				tResource = (Map<String, Object>)((List)tAnno.get("resource")).get(0);
			} else {
				tResource = (Map<String, Object>)tAnno.get("resource");
			}
			// do I need to change the format to html?
            if (tResource.get("@type") != null && tResource.get("@type").equals("cnt:ContentAsText") || tResource.get("format") != null && tResource.get("format").equals("text/plain")) {
    			tResource.put("@type","dctypes:Text"); //requried for Mirador: js/src/annotations/osd-canvas-renderer.js:421:if (value["@type"] === "dctypes:Text") {
    			tResource.put("format","text/html");
                String tText = (String)tResource.get("chars");
    			if (!tText.trim().startsWith("<p>")) {
    				tResource.put("chars", "<p>" + tText + "</p>");
    			} else {
    				tResource.put("chars", tText);
    			}
            }
			// Not sure if this is strictly necessary:
			/*List<String> tMotivation = new ArrayList<String>();
			tMotivation.add("oa:commenting");
			tAnno.put("motivation", tMotivation); // replaces painting with commenting*/


			if (tAnno.get("on") instanceof String) {
				String[] tOnStr = ((String)tAnno.get("on")).split("#");

				Map<String,Object> tOnObj = new HashMap<String,Object>();
				tOnObj.put("@type", "oa:SpecificResource");
				tOnObj.put("full", tOnStr[0]);

				Map<String,Object> tSelector = new HashMap<String,Object>();
				tOnObj.put("selector", tSelector);
				tSelector.put("@type", "oa:FragmentSelector");
				tSelector.put("value", tOnStr[1]);

				tAnno.put("on", tOnObj);
			}

			if (_encoder != null) {
				_encoder.encode(tAnno);
			}
		}

        _logger.debug("Normalised annotation list:");
        _logger.debug(JsonUtils.toPrettyString(tAnnotations));
		return tAnnotations;
	}
    protected String getTarget(final Map<String, Object> pAnno) {
        if (pAnno.get("on") instanceof String) {
            String tTarget = (String)pAnno.get("on");
            if (tTarget.contains("#")) {
                return tTarget.substring(0,tTarget.indexOf("#"));
            } else {
                return tTarget;
            }
        } else {
            return (String)((Map<String,Object>)pAnno.get("on")).get("full");
        }
    }

    public static String getHash(String txt, String hashType) throws IOException {
        try {
            java.security.MessageDigest md = java.security.MessageDigest.getInstance(hashType);
            byte[] array = md.digest(txt.getBytes("UTF-8"));
            StringBuilder hexString = new StringBuilder();

            for (int i = 0; i < array.length; i++) {
                String hex = Integer.toHexString(0xFF & array[i]);
                if (hex.length() == 1) {
                    hexString.append('0');
                }
                hexString.append(hex);
            }

            return hexString.toString();
        } catch (java.security.NoSuchAlgorithmException e) {
            throw new IOException("Failed to find algorithum " + hashType + " due to: " + e.toString());
        } catch (java.io.UnsupportedEncodingException tExcpt) {
            throw new IOException("Failed to convert string to md5 " + txt + " due to: " + tExcpt.toString());
        }
    }

	@SuppressWarnings("unchecked")
	public Map<String, Object> readAnnotaion(final InputStream pStream, final String pBaseURL) throws IOException {
		Object tAnnotation = JsonUtils.fromInputStream(pStream);
		Map<String, Object> tRoot = (Map<String,Object>)tAnnotation;

		if (tRoot.get("@id") == null) {
			String tID = pBaseURL + "/" + this.generateAnnoId();
			tRoot.put("@id", tID);
		}
		// Change context to local for quick processing
		tRoot.put("@context", this.getContext());

		if (_encoder != null) {
			_encoder.encode(tRoot);
		}
		return tRoot;
	}

	protected String getContext() {
        try {
    		return new File(_contextDir, "iiif-2.0.json").toURI().toURL().toString();
        } catch (IOException tExcpt) {
            _logger.error("Failed to find local context using external");
            return getExternalContext();

        }
	}

	public String getExternalContext() {
		return "http://iiif.io/api/presentation/2/context.json";
	}

	@SuppressWarnings("unchecked")
	protected Map<String, Object> buildAnnotationListHead() {
		Map<String, Object> tRoot = (Map<String, Object>)new HashMap<String,Object>();
		tRoot.put("@context", getExternalContext());
		String tPageId = "tmp";
		tRoot.put("@id", "http://dams.llgc.org.uk/iiif/annotation/list/" + tPageId);// current URL TODO make better id and resolvable
		tRoot.put("@type", "sc:AnnotationList");
		List tResources = new ArrayList();
		tRoot.put("resources", tResources);

		return tRoot;
	}

	public Map<String,Object> createAnnotationList(final Model pAnnotation) throws IOException, MalformedAnnotation {
		List<Model> tTmpList = new ArrayList<Model>();
		tTmpList.add(pAnnotation);

		return createAnnotationList(tTmpList).get(0);
	}

	public List<Map<String,Object>> createAnnotationList(final List<Model> pAnnotations) throws IOException, MalformedAnnotation {
		final Object contextJson = JsonUtils.fromInputStream(new FileInputStream(new File(_contextDir, "annotation_frame.json")));
		((Map<String, Object>)contextJson).put("@context", this.getContext());

		final JsonLdOptions options = new JsonLdOptions();
		options.format = "application/jsonld";

		Map<String, Object> tRoot = this.buildAnnotationListHead();
		List<Map<String,Object>> tResources = (List<Map<String,Object>>)tRoot.get("resources");
		for (Model tAnnotation : pAnnotations) {
            Annotation tAnno = null;
			try {
				tAnno = new Annotation(this.frameAnnotation(tAnnotation, false));
				if (tAnno != null && tAnno.checkValid() != "") { // Only add if its a valid annotation
					tResources.add(tAnno.toJson());
				}
			} catch (JsonLdError tExcpt) {
				_logger.error("Failed to generate Model " + tAnno.getId() + "  due to " + tExcpt);
				tExcpt.printStackTrace();
			}
		}

		return tResources;//tRoot;
	}

	public Model convertAnnoToModel(final Map<String,Object> pJson) throws IOException {
		if (pJson.get("@context") == null) {
			pJson.put("@context", this.getContext());
		} else if (pJson.get("@context") instanceof String && ((String)pJson.get("@context")).trim().startsWith("file://")) {
			File tContext = new File(((String)pJson.get("@context")).trim().substring("file://".length()));
			if (!tContext.exists()) {
				pJson.put("@context", this.getContext());
			}
		}
		String tJson = JsonUtils.toString(pJson);

		Model tJsonLDModel = ModelFactory.createDefaultModel();
		RDFDataMgr.read(tJsonLDModel, new ByteArrayInputStream(tJson.getBytes(Charset.forName("UTF-8"))), Lang.JSONLD);

		return tJsonLDModel;
	}

	public Map<String,Object> frameAnnotation(final Model pAnno, final boolean pCollapse) throws JsonLdError, IOException  {
		final Map<String,Object> contextJson = (Map<String,Object>)JsonUtils.fromInputStream(new FileInputStream(new File(_contextDir, "annotation_frame.json")));
		Map<String,Object> tJsonLd = this.frame(pAnno, contextJson);
		if (pCollapse) {
			this.colapseFragement(tJsonLd);
		}
		Map<String, Object> tOn = null;
		if (tJsonLd.get("on") instanceof Map) {
			tOn = (Map<String, Object>)tJsonLd.get("on");
			if (tOn.get("selector") != null && ((Map<String,Object>)tOn.get("selector")).get("value") instanceof List || tOn.get("source") instanceof List) {
				_logger.error("Annotation is broken " + tJsonLd.get("@id"));
				return tJsonLd;
			}
            // Autmoatically fix mirador-2.1.4 annos as if its done in javascript then exports won't include fix so won't work in Mirador
            // Do it automatically for mirador2.1.4 annos rather than using an encoder as we only want to fix this kind
            // of annos rather than all of them.
            if (((Map<String,Object>)tJsonLd.get("on")).get("selector") != null && ((Map<String,Object>)tJsonLd.get("on")).get("selector") != null && ((Map<String,Object>)((Map<String,Object>)tJsonLd.get("on")).get("selector")).get("item") != null) {
                Encoder tEncoder = new Mirador214();
                tEncoder.encode(tJsonLd);
            }
		}
		// Check if this is a valid annotation
		// if it is valid it should have one source, one fragment selector
		if (_encoder != null) {
			_encoder.decode(tJsonLd);
		}
		return tJsonLd;
	}


	public Map<String,Object> frameManifest(final Model pManifest) throws JsonLdError, IOException  {
		final Map<String,Object> tContextJson = (Map<String,Object>)JsonUtils.fromInputStream(new URL("https://iiif.io/api/presentation/2/manifest_frame.json").openStream());
		return this.frame(pManifest, tContextJson);
	}

	public Map<String,Object> frame(final Model pModel, final Map<String,Object> pFrame) throws JsonLdError, IOException {
		pFrame.put("@context", this.getContext());

		final JsonLdOptions tOptions = new JsonLdOptions();
		tOptions.format = "application/jsonld";
		tOptions.setPruneBlankNodeIdentifiers(true);

		StringWriter tStringOut = new StringWriter();
        if (pModel.supportsTransactions()) {
            pModel.begin();
        }
		RDFDataMgr.write(tStringOut, pModel, Lang.JSONLD);
        if (pModel.supportsTransactions()) {
            pModel.commit();
        }
		Map<String,Object> tFramed = (Map<String,Object>)JsonLdProcessor.frame(JsonUtils.fromString(tStringOut.toString()), pFrame,  tOptions);

		Map<String,Object> tJsonLd = (Map<String,Object>)((List)tFramed.get("@graph")).get(0);
		if (tJsonLd.get("@context") == null) {
			tJsonLd.put("@context", this.getExternalContext());
		}
		return tJsonLd;
	}

	// Need to move fragement into on
	public void colapseFragement(final Map<String,Object> pAnnotationJson) {
		if (pAnnotationJson.get("on") instanceof Map) {
            collapseFragmentOn(pAnnotationJson, (Map<String,Object>)pAnnotationJson.get("on"));
		} else if (pAnnotationJson.get("on") instanceof List) {
            for (Map<String,Object> tOn : (List<Map<String,Object>>)pAnnotationJson.get("on")) {
                collapseFragmentOn(pAnnotationJson, tOn);
            }
		}	// otherwise its already collapsed as its a string
	}
	public void collapseFragmentOn(final Map<String,Object> pAnnotationJson, final Map<String,Object> pOn) {
		if (pOn.get("selector") != null) {
			try {
                String tFragement = "";
                if (((Map)pOn.get("selector")).get("value") != null) {
    				tFragement = (String)((Map)pOn.get("selector")).get("value");
                } else {
    				tFragement = (String)((Map)((Map)pOn.get("selector")).get("default")).get("value");
                }
				String tTarget = (String)pOn.get("full");
				pAnnotationJson.put("on", tTarget + "#" + tFragement);
			} catch (ClassCastException tExcpt) {
				System.err.println("Failed to transform annotation");
				try {
					System.out.println(JsonUtils.toPrettyString(pAnnotationJson));
				} catch (IOException	tIOExcpt) {
					System.out.println("Failed to print failing annotation " + tIOExcpt);
				}
				throw tExcpt;
			}
		}
	}

	protected String generateAnnoId() {
		return "" + new java.util.Date().getTime();
	}
}
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.org.llgc.annotation.store.data.PageAnnoCount;
import uk.org.llgc.annotation.store.data.Manifest;
import uk.org.llgc.annotation.store.data.Canvas;
import uk.org.llgc.annotation.store.data.Target;
import uk.org.llgc.annotation.store.data.Annotation;
import uk.org.llgc.annotation.store.data.AnnotationList;
import uk.org.llgc.annotation.store.data.IIIFSearchResults;
import uk.org.llgc.annotation.store.data.SearchQuery;
import uk.org.llgc.annotation.store.exceptions.IDConflictException;
import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation;
import uk.org.llgc.annotation.store.AnnotationUtils;
import com.github.jsonldjava.utils.JsonUtils;
import org.apache.jena.query.ReadWrite;
import java.io.IOException;
import java.util.Map;
import java.util.List;
package uk.org.llgc.annotation.store.adapters;







public abstract class AbstractStoreAdapter implements StoreAdapter {
    protected static Logger _logger = LogManager.getLogger(AbstractStoreAdapter.class.getName());
	protected AnnotationUtils _annoUtils = null;

	public void init(final AnnotationUtils pAnnoUtils) {
		_annoUtils = pAnnoUtils;
	}

	public AnnotationList addAnnotationList(final AnnotationList pAnnoList) throws IOException, IDConflictException, MalformedAnnotation {