diff --git a/services/src/edu/cornell/mannlib/vitro/webservices/serializers/EntitySerializer.java b/services/src/edu/cornell/mannlib/vitro/webservices/serializers/EntitySerializer.java index b1bf4e5a8..db5b254c9 100644 --- a/services/src/edu/cornell/mannlib/vitro/webservices/serializers/EntitySerializer.java +++ b/services/src/edu/cornell/mannlib/vitro/webservices/serializers/EntitySerializer.java @@ -38,7 +38,6 @@ public class EntitySerializer implements Serializer { public static final String IMAGEFILE_MBER= "imageFile"; public static final String ANCHOR_MBER= "anchor"; public static final String BLURB_MBER= "blurb"; - public static final String IMAGETHUMB_MBER= "imageThumb"; public static final String CITATION_MBER= "citation"; public static final String STATUS_MBER= "status"; public static final String PROPERTYLIST_MBER= "propertyList"; @@ -82,10 +81,9 @@ public class EntitySerializer implements Serializer { context.serialize(new QName("", SUNRISE_MBER), null, ent.getSunrise()); context.serialize(new QName("", SUNSET_MBER), null, ent.getSunset()); context.serialize(new QName("", TIMEKEY_MBER), null, ent.getTimekey()); - context.serialize(new QName("", IMAGEFILE_MBER), null, ent.getImageFile()); + context.serialize(new QName("", IMAGEFILE_MBER), null, ent.getMainImageUri()); context.serialize(new QName("", ANCHOR_MBER), null, ent.getAnchor()); context.serialize(new QName("", BLURB_MBER), null, ent.getBlurb()); - context.serialize(new QName("", IMAGETHUMB_MBER), null, ent.getImageThumb()); context.serialize(new QName("", CITATION_MBER), null, ent.getCitation()); context.serialize(new QName("", STATUS_MBER), null, ent.getStatus()); context.serialize(new QName("", LINKSLIST_MBER), null, ent.getLinksList()); diff --git a/webapp/config/default.log4j.properties b/webapp/config/default.log4j.properties index e55941a39..c7b645225 100644 --- a/webapp/config/default.log4j.properties +++ b/webapp/config/default.log4j.properties @@ -37,3 +37,4 @@ log4j.logger.org.apache.catalina=INFO log4j.logger.org.diretwebremoting=ERROR log4j.logger.edu.cornell.mannlib.vitro.webapp.ConfigurationProperties=INFO +log4j.logger.edu.cornell.mannlib.vitro.webapp.filestorage.updater.FileStorageUpdater=INFO diff --git a/webapp/config/web.xml b/webapp/config/web.xml index 466bf0490..b63cd727f 100644 --- a/webapp/config/web.xml +++ b/webapp/config/web.xml @@ -74,13 +74,26 @@ - edu.cornell.mannlib.vitro.webapp.servlet.setup.UpdateKnowledgeBase + + + edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup + + + + + + + + + edu.cornell.mannlib.vitro.webapp.servlet.setup.UpdateUploadedFiles + + + - edu.cornell.mannlib.vitro.webapp.servlet.setup.AttachSubmodels @@ -143,10 +156,11 @@ edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreeMarkerSetup - + + - edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup + org.apache.commons.fileupload.servlet.FileCleanerCleanup @@ -166,6 +180,7 @@ --> + @@ -1053,13 +1068,16 @@ uploadImages edu.cornell.mannlib.vitro.webapp.controller.edit.UploadImagesServlet - - workspaceDir - - /usr/local/src/Vitro/dream/common/web - - + + + serveFiles + edu.cornell.mannlib.vitro.webapp.filestorage.serving.FileServingServlet + + + serveFiles + /file/* + generic_editprep diff --git a/webapp/ontologies/system/vitroPublic.owl b/webapp/ontologies/system/vitroPublic.owl index 7f44d4b94..d1f51520a 100644 --- a/webapp/ontologies/system/vitroPublic.owl +++ b/webapp/ontologies/system/vitroPublic.owl @@ -55,15 +55,15 @@ - + - + - + \ No newline at end of file diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/CuratorEditingPolicy.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/CuratorEditingPolicy.java index 5582b9528..a05ba592f 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/CuratorEditingPolicy.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/CuratorEditingPolicy.java @@ -109,8 +109,7 @@ public class CuratorEditingPolicy implements VisitingPolicyIface { this.editableVitroUris.add(VitroVocabulary.TIMEKEY); this.editableVitroUris.add(VitroVocabulary.CITATION); - this.editableVitroUris.add(VitroVocabulary.IMAGEFILE); - this.editableVitroUris.add(VitroVocabulary.IMAGETHUMB); + this.editableVitroUris.add(VitroVocabulary.IND_MAIN_IMAGE); this.editableVitroUris.add(VitroVocabulary.LINK); this.editableVitroUris.add(VitroVocabulary.PRIMARY_LINK); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/DbAdminEditingPolicy.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/DbAdminEditingPolicy.java index e41113e94..a6ea0cc59 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/DbAdminEditingPolicy.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/DbAdminEditingPolicy.java @@ -112,8 +112,7 @@ public class DbAdminEditingPolicy implements VisitingPolicyIface { this.editableVitroUris.add(VitroVocabulary.TIMEKEY); this.editableVitroUris.add(VitroVocabulary.CITATION); - this.editableVitroUris.add(VitroVocabulary.IMAGEFILE); - this.editableVitroUris.add(VitroVocabulary.IMAGETHUMB); + this.editableVitroUris.add(VitroVocabulary.IND_MAIN_IMAGE); this.editableVitroUris.add(VitroVocabulary.LINK); this.editableVitroUris.add(VitroVocabulary.PRIMARY_LINK); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/EditorEditingPolicy.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/EditorEditingPolicy.java index 2026dc172..54d6a96bd 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/EditorEditingPolicy.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/EditorEditingPolicy.java @@ -110,8 +110,7 @@ public class EditorEditingPolicy implements VisitingPolicyIface{ this.editableVitroUris.add(VitroVocabulary.TIMEKEY); this.editableVitroUris.add(VitroVocabulary.CITATION); - this.editableVitroUris.add(VitroVocabulary.IMAGEFILE); - this.editableVitroUris.add(VitroVocabulary.IMAGETHUMB); + this.editableVitroUris.add(VitroVocabulary.IND_MAIN_IMAGE); this.editableVitroUris.add(VitroVocabulary.LINK); this.editableVitroUris.add(VitroVocabulary.PRIMARY_LINK); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/SelfEditingPolicy.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/SelfEditingPolicy.java index 408f8586e..c3bf0f166 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/SelfEditingPolicy.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/SelfEditingPolicy.java @@ -5,8 +5,6 @@ package edu.cornell.mannlib.vitro.webapp.auth.policy; import java.util.Collections; import java.util.HashSet; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -110,8 +108,7 @@ public class SelfEditingPolicy implements VisitingPolicyIface { this.editableVitroUris.add(VitroVocabulary.TIMEKEY); this.editableVitroUris.add(VitroVocabulary.CITATION); - this.editableVitroUris.add(VitroVocabulary.IMAGEFILE); - this.editableVitroUris.add(VitroVocabulary.IMAGETHUMB); + this.editableVitroUris.add(VitroVocabulary.IND_MAIN_IMAGE); this.editableVitroUris.add(VitroVocabulary.LINK); this.editableVitroUris.add(VitroVocabulary.PRIMARY_LINK); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/Individual.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/Individual.java index ac0c5ba69..bf3e81071 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/Individual.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/Individual.java @@ -93,11 +93,11 @@ public interface Individual extends ResourceBean, VitroTimeWindowedResource, Com String getStatus(); void setStatus(String s); - String getImageFile(); - void setImageFile(String imageFile); - - String getImageThumb(); - void setImageThumb(String imageThumb); + void setMainImageUri(String mainImageUri); + String getMainImageUri(); + + String getImageUrl(); + String getThumbUrl(); String getUrl(); void setUrl(String url); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/IndividualImpl.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/IndividualImpl.java index d79c5f941..e5aa93d79 100755 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/IndividualImpl.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/IndividualImpl.java @@ -1,132 +1,141 @@ /* $This file is distributed under the terms of the license in /doc/license.txt$ */ -package edu.cornell.mannlib.vitro.webapp.beans; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.lang.reflect.Method; -import java.sql.Timestamp; -import java.text.Collator; -import java.util.*; - -/** - * Represents a single entity record. -*/ -public class IndividualImpl extends BaseResourceBean implements Individual, Comparable { - public String name = null; - public String vClassURI = null; - protected VClass vClass = null; - protected List directVClasses = null; - protected List allVClasses = null; - protected Date sunrise = null; - protected Date sunset = null; - protected Date timekey = null; - protected Timestamp modTime = null; - protected List propertyList = null; - protected Map objectPropertyMap = null; - protected List datatypePropertyList = null; - protected Map dataPropertyMap = null; - protected List dataPropertyStatements = null; - protected List objectPropertyStatements = null; - protected List rangeEnts2Ents = null; - protected List externalIds = null; - - protected String moniker = null; - protected String url = null; - protected String description = null; - protected String imageFile = null; - protected String anchor = null; - protected String blurb = null; - protected String imageThumb = null; - protected String citation = null; - protected int statusId = 0; - protected String status = null; - protected List linksList = null; - protected Link primaryLink = null; - protected List keywords=null; - protected List keywordObjects=null; - protected Float searchBoost; - - /** indicates if sortForDisplay has been called */ - protected boolean sorted = false; - protected boolean DIRECT = true; - protected boolean ALL = false; - - public IndividualImpl() { - } - - public IndividualImpl(String URI) { - this.setURI(URI); - this.setVClasses(new ArrayList(), DIRECT); - this.setVClasses(new ArrayList(), ALL); - this.setObjectPropertyStatements(new ArrayList()); - this.setObjectPropertyMap(new HashMap()); - this.setDataPropertyStatements(new ArrayList()); - this.setDataPropertyMap(new HashMap()); - this.setPropertyList(new ArrayList()); - this.setDatatypePropertyList(new ArrayList()); - } - - public String getName(){return name;} - public void setName(String in){name=in;} - -// private String modTime = null; -// public String getModtime(){return modTime;} -// public void setModtime(String in){modTime=in;} - - public String getVClassURI(){return vClassURI;} - public void setVClassURI(String in){vClassURI=in;} - - public Date getSunrise(){return sunrise;} - public void setSunrise(Date in){sunrise=in;} - - public Date getSunset(){return sunset;} - public void setSunset(Date in){sunset=in;} - - public Date getTimekey(){return timekey;} - public void setTimekey(Date in){timekey=in;} - - /** - * Returns the last time this object was changed in the model. - * Notice Java API craziness: Timestamp is a subclass of Date - * but there are notes in the Javadoc that you should not pretend - * that a Timestamp is a Date. (Crazy ya?) In particular, - * Timestamp.equals(Date) will never return true because of - * the 'nanos.' - */ - public Timestamp getModTime(){return modTime;} - public void setModTime(Timestamp in){modTime=in;} - - public List getObjectPropertyList() { - return propertyList; - } - public void setPropertyList(List propertyList) { - this.propertyList = propertyList; - } - public Map getObjectPropertyMap() { - return this.objectPropertyMap; - } - public void setObjectPropertyMap( Map propertyMap ) { - this.objectPropertyMap = propertyMap; - } - public List getDataPropertyList() { - return datatypePropertyList; - } - public void setDatatypePropertyList(List datatypePropertyList) { - this.datatypePropertyList = datatypePropertyList; - } - public Map getDataPropertyMap() { - return this.dataPropertyMap; - } - public void setDataPropertyMap( Map propertyMap ) { - this.dataPropertyMap = propertyMap; - } - public void setDataPropertyStatements(List list) { - dataPropertyStatements = list; - } - public List getDataPropertyStatements(){ - return dataPropertyStatements; +package edu.cornell.mannlib.vitro.webapp.beans; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Method; +import java.sql.Timestamp; +import java.text.Collator; +import java.util.*; + +/** + * Represents a single entity record. +*/ +public class IndividualImpl extends BaseResourceBean implements Individual, Comparable { + /** + * This can be used as a "not initialized" indicator for a property that + * could validly be set to null. If get() is + * called on such a property, and the property has this value, the correct + * value can be fetched and cached. + */ + protected static final String NOT_INITIALIZED = "__%NOT_INITIALIZED%__"; + + public String name = null; + public String vClassURI = null; + protected VClass vClass = null; + protected List directVClasses = null; + protected List allVClasses = null; + protected Date sunrise = null; + protected Date sunset = null; + protected Date timekey = null; + protected Timestamp modTime = null; + protected List propertyList = null; + protected Map objectPropertyMap = null; + protected List datatypePropertyList = null; + protected Map dataPropertyMap = null; + protected List dataPropertyStatements = null; + protected List objectPropertyStatements = null; + protected List rangeEnts2Ents = null; + protected List externalIds = null; + + protected String moniker = null; + protected String url = null; + protected String description = null; + protected String anchor = null; + protected String blurb = null; + protected String mainImageUri = NOT_INITIALIZED; + protected String imageUrl; + protected String thumbUrl; + protected String citation = null; + protected int statusId = 0; + protected String status = null; + protected List linksList = null; + protected Link primaryLink = null; + protected List keywords=null; + protected List keywordObjects=null; + protected Float searchBoost; + + /** indicates if sortForDisplay has been called */ + protected boolean sorted = false; + protected boolean DIRECT = true; + protected boolean ALL = false; + + public IndividualImpl() { + } + + public IndividualImpl(String URI) { + this.setURI(URI); + this.setVClasses(new ArrayList(), DIRECT); + this.setVClasses(new ArrayList(), ALL); + this.setObjectPropertyStatements(new ArrayList()); + this.setObjectPropertyMap(new HashMap()); + this.setDataPropertyStatements(new ArrayList()); + this.setDataPropertyMap(new HashMap()); + this.setPropertyList(new ArrayList()); + this.setDatatypePropertyList(new ArrayList()); + } + + public String getName(){return name;} + public void setName(String in){name=in;} + +// private String modTime = null; +// public String getModtime(){return modTime;} +// public void setModtime(String in){modTime=in;} + + public String getVClassURI(){return vClassURI;} + public void setVClassURI(String in){vClassURI=in;} + + public Date getSunrise(){return sunrise;} + public void setSunrise(Date in){sunrise=in;} + + public Date getSunset(){return sunset;} + public void setSunset(Date in){sunset=in;} + + public Date getTimekey(){return timekey;} + public void setTimekey(Date in){timekey=in;} + + /** + * Returns the last time this object was changed in the model. + * Notice Java API craziness: Timestamp is a subclass of Date + * but there are notes in the Javadoc that you should not pretend + * that a Timestamp is a Date. (Crazy ya?) In particular, + * Timestamp.equals(Date) will never return true because of + * the 'nanos.' + */ + public Timestamp getModTime(){return modTime;} + public void setModTime(Timestamp in){modTime=in;} + + public List getObjectPropertyList() { + return propertyList; + } + public void setPropertyList(List propertyList) { + this.propertyList = propertyList; + } + public Map getObjectPropertyMap() { + return this.objectPropertyMap; + } + public void setObjectPropertyMap( Map propertyMap ) { + this.objectPropertyMap = propertyMap; + } + public List getDataPropertyList() { + return datatypePropertyList; + } + public void setDatatypePropertyList(List datatypePropertyList) { + this.datatypePropertyList = datatypePropertyList; + } + public Map getDataPropertyMap() { + return this.dataPropertyMap; + } + public void setDataPropertyMap( Map propertyMap ) { + this.dataPropertyMap = propertyMap; + } + public void setDataPropertyStatements(List list) { + dataPropertyStatements = list; + } + public List getDataPropertyStatements(){ + return dataPropertyStatements; } public List getDataPropertyStatements(String propertyUri) { @@ -158,41 +167,41 @@ public class IndividualImpl extends BaseResourceBean implements Individual, Comp public String getDataValue(String propertyUri) { List stmts = getDataPropertyStatements(propertyUri); return stmts.isEmpty() ? null : stmts.get(0).getData(); - } - - public VClass getVClass() { - return vClass; - } - public void setVClass(VClass class1) { - vClass = class1; - } - - public List getVClasses() { - return allVClasses; - } - - public List getVClasses(boolean direct) { - if (direct) { - return directVClasses; - } else { - return allVClasses; - } - } - - public void setVClasses(List vClassList, boolean direct) { - if (direct) { - this.directVClasses = vClassList; - } else { - this.allVClasses = vClassList; - } - } - - public void setObjectPropertyStatements(List list) { - objectPropertyStatements = list; - } - - public List getObjectPropertyStatements(){ - return objectPropertyStatements; + } + + public VClass getVClass() { + return vClass; + } + public void setVClass(VClass class1) { + vClass = class1; + } + + public List getVClasses() { + return allVClasses; + } + + public List getVClasses(boolean direct) { + if (direct) { + return directVClasses; + } else { + return allVClasses; + } + } + + public void setVClasses(List vClassList, boolean direct) { + if (direct) { + this.directVClasses = vClassList; + } else { + this.allVClasses = vClassList; + } + } + + public void setObjectPropertyStatements(List list) { + objectPropertyStatements = list; + } + + public List getObjectPropertyStatements(){ + return objectPropertyStatements; } public List getObjectPropertyStatements(String propertyUri) { @@ -218,228 +227,239 @@ public class IndividualImpl extends BaseResourceBean implements Individual, Comp public Individual getRelatedIndividual(String propertyUri) { List stmts = getObjectPropertyStatements(propertyUri); return stmts.isEmpty() ? null : stmts.get(0).getObject(); - } - - public List getExternalIds(){ - return externalIds; - } - public void setExternalIds(List externalIds){ - this.externalIds = externalIds; - } - - - public String getMoniker(){return moniker;} - public void setMoniker(String in){moniker=in;} - - public String getDescription(){return description;} - public void setDescription(String in){description=in;} - - public String getAnchor(){return anchor;} - public void setAnchor(String in){anchor=in;} - - public String getBlurb(){return blurb;} - public void setBlurb(String in){blurb=in;} - - public String getCitation(){return citation;} - public void setCitation(String in){citation=in;} - - public int getStatusId(){return statusId;} - public void setStatusId(int in){statusId=in;} - - public String getStatus() {return status;} - public void setStatus(String s) {status=s; } - - public String getImageFile() { - return imageFile; - } - public void setImageFile(String imageFile) { - this.imageFile = imageFile; - } - public String getImageThumb() { - return imageThumb; - } - public void setImageThumb(String imageThumb) { - this.imageThumb = imageThumb; - } - public String getUrl() { - return url; - } - public void setUrl(String url) { - this.url = url; - } - public List getLinksList() { - return linksList; - } - public void setLinksList(List linksList) { - this.linksList = linksList; - } - - public Link getPrimaryLink() { - return primaryLink; - } - - public void setPrimaryLink(Link link) { - primaryLink = link; - } - - - /* look at PortalFlag.numeric2numerics if you want to know which - * bits are set in a numeric flag. - * - * NOTICE: - * Values set Entity.getFlagXNumeric() will NOT be saved to the model. - * - * Also, changes to an entity flag state using Entity.setFlagXNumeric() - * are not reflected in Entity.getFlagXSet() and vice versa. - */ - protected String flag1Set = null; - public String getFlag1Set(){return flag1Set;} - public void setFlag1Set(String in){flag1Set=in;} - - protected int flag1Numeric = -1; - public int getFlag1Numeric(){return flag1Numeric;} - public void setFlag1Numeric(int i){flag1Numeric=i;} - - /* Consider the flagBitMask as a mask to & with flags. - if flagBitMask bit zero is set then return true if - the individual is in portal 2, - if flagBitMask bit 1 is set then return true if - the individua is in portal 4 - etc. - */ - public boolean doesFlag1Match(int flagBitMask) { - return (flagBitMask & getFlag1Numeric()) != 0; - } - - protected String flag2Set = null; - public String getFlag2Set(){return flag2Set;} - public void setFlag2Set(String in){flag2Set=in;} - - protected int flag2Numeric = -1; - public int getFlag2Numeric(){return flag2Numeric;} - public void setFlag2Numeric(int i){flag2Numeric=i;} - - protected String flag3Set = null; - public String getFlag3Set(){return flag3Set;} - public void setFlag3Set(String in){flag3Set=in;} - - protected int flag3Numeric = -1; - public int getFlag3Numeric(){return flag3Numeric;} - public void setFlag3Numeric(int i){flag3Numeric=i;} - - public List getKeywords() { return keywords; } - public void setKeywords(List keywords) {this.keywords = keywords;} - public String getKeywordString(){ - String rv = ""; - List keywords=getKeywords(); - if (getKeywords()!=null){ - Iterator it1 = getKeywords().iterator(); - TreeSet keywordSet = new TreeSet(new Comparator() { - public int compare( String first, String second ) { - if (first==null) { - return 1; - } - if (second==null) { - return -1; - } - Collator collator = Collator.getInstance(); - return collator.compare(first,second); - } - }); - while( it1.hasNext() ){ - keywordSet.add(it1.next()); - } - Iterator it2 = keywordSet.iterator(); - while (it2.hasNext()) { - rv+= it2.next(); - if( it2.hasNext()) - rv+=", "; - } - } - return rv; - } - public List getKeywordObjects() { return keywordObjects; } - public void setKeywordObjects(List keywords) {this.keywordObjects = keywords;} - - public Float getSearchBoost() { return searchBoost; } - public void setSearchBoost(Float boost) { searchBoost = boost; } - - /** - * Sorts the ents2ents records into the proper order for display. - * - */ - public void sortForDisplay(){ - if( sorted ) return; - if( getObjectPropertyList() == null ) return; - sortPropertiesForDisplay(); - sortEnts2EntsForDisplay(); - sorted = true; - } - - protected void sortEnts2EntsForDisplay(){ - if( getObjectPropertyList() == null ) return; - - Iterator it = getObjectPropertyList().iterator(); - while(it.hasNext()){ - ObjectProperty prop = (ObjectProperty)it.next(); - prop.sortObjectPropertyStatementsForDisplay(prop,prop.getObjectPropertyStatements()); - } - } - - protected void sortPropertiesForDisplay( ){ - //here we sort the Property objects - Collections.sort(getObjectPropertyList(), new ObjectProperty.DisplayComparator()); - } - - public static final String [] INCLUDED_IN_JSON = { - "URI", - "name", - "moniker", - "vClassId" - }; - - - public JSONObject toJSON() throws JSONException { - JSONObject jsonObj = new JSONObject(this, INCLUDED_IN_JSON); - return jsonObj; - } - /** - * - * @param fieldName- expected to be the field name in the format - * @return - * @throws NoSuchMethodException - */ - public Object getField(String fieldName) throws NoSuchMethodException{ - if( fieldName == null || fieldName.length() == 0) return null; - - if( "name".equalsIgnoreCase(fieldName) ) - return getName(); - if( "timekey".equalsIgnoreCase(fieldName) ) - return getTimekey(); - - //not one of the more common ones, try reflection - - //capitalize first letter - String methodName = "get" + fieldName.substring(0,1).toUpperCase() - + fieldName.substring(1,fieldName.length()); - - Class cls = this.getClass(); - try { - Method meth = cls.getMethod(methodName, (Class[]) null); - return meth.invoke(this,(Object[])null); - } catch (Exception e) { } - //should never get here - throw new NoSuchMethodException("Entity.getField() attempt to use a method called " - + methodName +"() for field " + fieldName + " but the method doesn't exist."); - } - - public int compareTo(Individual o2) { - Collator collator = Collator.getInstance(); - if (o2 == null) { - return 1; - } else { - return collator.compare(this.getName(),o2.getName()); - } - } - -} + } + + public List getExternalIds(){ + return externalIds; + } + public void setExternalIds(List externalIds){ + this.externalIds = externalIds; + } + + + public String getMoniker(){return moniker;} + public void setMoniker(String in){moniker=in;} + + public String getDescription(){return description;} + public void setDescription(String in){description=in;} + + public String getAnchor(){return anchor;} + public void setAnchor(String in){anchor=in;} + + public String getBlurb(){return blurb;} + public void setBlurb(String in){blurb=in;} + + public String getCitation(){return citation;} + public void setCitation(String in){citation=in;} + + public int getStatusId(){return statusId;} + public void setStatusId(int in){statusId=in;} + + public String getStatus() {return status;} + public void setStatus(String s) {status=s; } + + + @Override + public String getMainImageUri() { + return (mainImageUri == NOT_INITIALIZED) ? null : mainImageUri; + } + + @Override + public void setMainImageUri(String mainImageUri) { + this.mainImageUri = mainImageUri; + this.imageUrl = null; + this.thumbUrl = null; + } + + @Override + public String getImageUrl() { + return "imageUrl"; + } + + @Override + public String getThumbUrl() { + return "thumbUrl"; + } + + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } + public List getLinksList() { + return linksList; + } + public void setLinksList(List linksList) { + this.linksList = linksList; + } + + public Link getPrimaryLink() { + return primaryLink; + } + + public void setPrimaryLink(Link link) { + primaryLink = link; + } + + + /* look at PortalFlag.numeric2numerics if you want to know which + * bits are set in a numeric flag. + * + * NOTICE: + * Values set Entity.getFlagXNumeric() will NOT be saved to the model. + * + * Also, changes to an entity flag state using Entity.setFlagXNumeric() + * are not reflected in Entity.getFlagXSet() and vice versa. + */ + protected String flag1Set = null; + public String getFlag1Set(){return flag1Set;} + public void setFlag1Set(String in){flag1Set=in;} + + protected int flag1Numeric = -1; + public int getFlag1Numeric(){return flag1Numeric;} + public void setFlag1Numeric(int i){flag1Numeric=i;} + + /* Consider the flagBitMask as a mask to & with flags. + if flagBitMask bit zero is set then return true if + the individual is in portal 2, + if flagBitMask bit 1 is set then return true if + the individua is in portal 4 + etc. + */ + public boolean doesFlag1Match(int flagBitMask) { + return (flagBitMask & getFlag1Numeric()) != 0; + } + + protected String flag2Set = null; + public String getFlag2Set(){return flag2Set;} + public void setFlag2Set(String in){flag2Set=in;} + + protected int flag2Numeric = -1; + public int getFlag2Numeric(){return flag2Numeric;} + public void setFlag2Numeric(int i){flag2Numeric=i;} + + protected String flag3Set = null; + public String getFlag3Set(){return flag3Set;} + public void setFlag3Set(String in){flag3Set=in;} + + protected int flag3Numeric = -1; + public int getFlag3Numeric(){return flag3Numeric;} + public void setFlag3Numeric(int i){flag3Numeric=i;} + + public List getKeywords() { return keywords; } + public void setKeywords(List keywords) {this.keywords = keywords;} + public String getKeywordString(){ + String rv = ""; + List keywords=getKeywords(); + if (getKeywords()!=null){ + Iterator it1 = getKeywords().iterator(); + TreeSet keywordSet = new TreeSet(new Comparator() { + public int compare( String first, String second ) { + if (first==null) { + return 1; + } + if (second==null) { + return -1; + } + Collator collator = Collator.getInstance(); + return collator.compare(first,second); + } + }); + while( it1.hasNext() ){ + keywordSet.add(it1.next()); + } + Iterator it2 = keywordSet.iterator(); + while (it2.hasNext()) { + rv+= it2.next(); + if( it2.hasNext()) + rv+=", "; + } + } + return rv; + } + public List getKeywordObjects() { return keywordObjects; } + public void setKeywordObjects(List keywords) {this.keywordObjects = keywords;} + + public Float getSearchBoost() { return searchBoost; } + public void setSearchBoost(Float boost) { searchBoost = boost; } + + /** + * Sorts the ents2ents records into the proper order for display. + * + */ + public void sortForDisplay(){ + if( sorted ) return; + if( getObjectPropertyList() == null ) return; + sortPropertiesForDisplay(); + sortEnts2EntsForDisplay(); + sorted = true; + } + + protected void sortEnts2EntsForDisplay(){ + if( getObjectPropertyList() == null ) return; + + Iterator it = getObjectPropertyList().iterator(); + while(it.hasNext()){ + ObjectProperty prop = (ObjectProperty)it.next(); + prop.sortObjectPropertyStatementsForDisplay(prop,prop.getObjectPropertyStatements()); + } + } + + protected void sortPropertiesForDisplay( ){ + //here we sort the Property objects + Collections.sort(getObjectPropertyList(), new ObjectProperty.DisplayComparator()); + } + + public static final String [] INCLUDED_IN_JSON = { + "URI", + "name", + "moniker", + "vClassId" + }; + + + public JSONObject toJSON() throws JSONException { + JSONObject jsonObj = new JSONObject(this, INCLUDED_IN_JSON); + return jsonObj; + } + /** + * + * @param fieldName- expected to be the field name in the format + * @return + * @throws NoSuchMethodException + */ + public Object getField(String fieldName) throws NoSuchMethodException{ + if( fieldName == null || fieldName.length() == 0) return null; + + if( "name".equalsIgnoreCase(fieldName) ) + return getName(); + if( "timekey".equalsIgnoreCase(fieldName) ) + return getTimekey(); + + //not one of the more common ones, try reflection + + //capitalize first letter + String methodName = "get" + fieldName.substring(0,1).toUpperCase() + + fieldName.substring(1,fieldName.length()); + + Class cls = this.getClass(); + try { + Method meth = cls.getMethod(methodName, (Class[]) null); + return meth.invoke(this,(Object[])null); + } catch (Exception e) { } + //should never get here + throw new NoSuchMethodException("Entity.getField() attempt to use a method called " + + methodName +"() for field " + fieldName + " but the method doesn't exist."); + } + + public int compareTo(Individual o2) { + Collator collator = Collator.getInstance(); + if (o2 == null) { + return 1; + } else { + return collator.compare(this.getName(),o2.getName()); + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/EntityController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/EntityController.java index 6daa5da4e..587d1b90e 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/EntityController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/EntityController.java @@ -25,7 +25,6 @@ import com.hp.hpl.jena.ontology.OntModel; import com.hp.hpl.jena.rdf.model.Literal; import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.ModelFactory; -import com.hp.hpl.jena.rdf.model.ModelMaker; import com.hp.hpl.jena.rdf.model.Property; import com.hp.hpl.jena.rdf.model.RDFNode; import com.hp.hpl.jena.rdf.model.Resource; @@ -42,6 +41,10 @@ import edu.cornell.mannlib.vitro.webapp.beans.Portal; import edu.cornell.mannlib.vitro.webapp.beans.VClass; import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileServingHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; import edu.cornell.mannlib.vitro.webapp.search.beans.VitroQuery; import edu.cornell.mannlib.vitro.webapp.search.beans.VitroQueryWrapper; import edu.cornell.mannlib.vitro.webapp.utils.NamespaceMapper; @@ -97,6 +100,13 @@ public class EntityController extends VitroHttpServlet { doNotFound(vreq, res); return; } + + // If this is an uploaded file, redirect to its "alias URL". + String aliasUrl = getAliasUrlForBytestreamIndividual(indiv); + if (aliasUrl != null) { + res.sendRedirect(req.getContextPath() + aliasUrl); + return; + } doHtml( vreq, res , indiv); return; @@ -476,6 +486,47 @@ public class EntityController extends VitroHttpServlet { // TODO Auto-generated method stub return false; } + + /** + * If this entity represents a File Bytestream, get its alias URL so we can + * properly serve the file contents. + */ + private String getAliasUrlForBytestreamIndividual(Individual entity) + throws IOException { + if (!FileModelHelper.isFileBytestream(entity)) { + log.debug("Entity at '" + entity.getURI() + + "' is not recognized as a FileByteStream."); + return null; + } + + FileStorage fs = (FileStorage) getServletContext().getAttribute( + FileStorageSetup.ATTRIBUTE_NAME); + if (fs == null) { + log.error("Servlet context does not contain file storage at '" + + FileStorageSetup.ATTRIBUTE_NAME + "'"); + return null; + } + + String filename = fs.getFilename(entity.getURI()); + if (filename == null) { + log.error("Entity at '" + entity.getURI() + + "' is recognized as a FileByteStream, " + + "but the file system does not recognize it."); + return null; + } + + String url = FileServingHelper.getBytestreamAliasUrl(entity.getURI(), + filename); + if (url.equals(entity.getURI())) { + log.error("Entity at '" + entity.getURI() + + "' is recognized as a FileByteStream, " + + "but can't be translated to an alias URL."); + return null; + } + + log.debug("Alias URL for '" + entity.getURI() + "' is '" + url + "'"); + return url; + } private Model getRDF(Individual entity, OntModel contextModel, Model newModel, int recurseDepth ) { Resource subj = newModel.getResource(entity.getURI()); @@ -578,7 +629,7 @@ public class EntityController extends VitroHttpServlet { out.println("

id is the id of the entity to query for. netid also works.

"); out.println(""); } - + private void doNotFound(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { VitroRequest vreq = new VitroRequest(req); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/CloneEntityServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/CloneEntityServlet.java index 078d89313..298fc041f 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/CloneEntityServlet.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/CloneEntityServlet.java @@ -116,8 +116,6 @@ public class CloneEntityServlet extends BaseEditController { ind.setBlurb(""); ind.setDescription(""); ind.setCitation(""); - ind.setImageFile(""); - ind.setImageThumb(""); String cloneURI=individualDao.insertNewIndividual(ind); if (cloneURI == null){ log.error("Error inserting cloned individual"); return; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/UploadImagesServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/UploadImagesServlet.java index 7d055bd12..e847126fb 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/UploadImagesServlet.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/edit/UploadImagesServlet.java @@ -2,600 +2,480 @@ package edu.cornell.mannlib.vitro.webapp.controller.edit; -/** - * @version 0.9 2004-01-29 - * @author Jon Corson-Rikert - * - * UPDATES: - * 2005-07-22 jc55 added support for entering remote image URL when uploading thumbnail image - * 2005-06-30 jc55 added support for home parameter - */ +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; -/************** DOCUMENTATION ********************* - * This servlet uses 3 directory locations on the server, which for illustrative purposes - * we assume has the Tomcat application context at /usr/local/tomcat/webapps/vivo - * and the source code and build files at /usr/local/src/Vitro/dream - * - * 1) workspaceDir: a temp directory where the file is uploaded to and reports are stored - * 2) websiteDir : a website directory where the image is copied so that it appears on the website immediately after upload - * 3) sourceDir : the directory in the source tree where the images are stored so that the context can be recreated and/or moved - * - */ +import javax.imageio.ImageIO; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; -import java.io.*; -import java.util.*; -import javax.servlet.http.*; -import javax.servlet.*; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.io.FilenameUtils; +import org.apache.log4j.Logger; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import edu.cornell.mannlib.vedit.beans.LoginFormBean; -import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.beans.Individual; import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; - -import com.oreilly.servlet.MultipartRequest; -import com.oreilly.servlet.multipart.DefaultFileRenamePolicy; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileAlreadyExistsException; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; +import edu.cornell.mannlib.vitro.webapp.filestorage.uploadrequest.FileUploadServletRequest; public class UploadImagesServlet extends VitroHttpServlet { - private static final Log log = LogFactory.getLog(UploadImagesServlet.class.getName()); - private String sourceDirName; // all uploaded images are copied to the source directory, not just the application context - private String websiteDirName; // the application context + private static final Logger log = Logger + .getLogger(UploadImagesServlet.class); - /** - * Notice that init() gets called the first time the servlet is requested, - * at least under tomcat 5.5. - */ - public void init(ServletConfig config) throws ServletException { - super.init(config); + /** Recognized file extensions mapped to MIME-types. */ + private static final Map RECOGNIZED_FILE_TYPES = createFileTypesMap(); - // something like: /usr/local/tomcat/webapps/vivo - websiteDirName = getServletContext().getRealPath(""); + /** The field in the HTTP request that holds the file. */ + public static final String FILE_FIELD_NAME = "file1"; - // something like: /usr/local/src/Vitro/dream/common/web - try{ - sourceDirName = getSourceDirName(); - }catch(Exception ex){ - log.error("initialization Exception: "+ex.getMessage()); - } + /** The field in the HTTP request that holds the individual's URI. */ + public static final String URI_FIELD_NAME = "entityUri"; - log.info("UploadImagesServlet initialized to copy uploaded images to source directory: " + sourceDirName); - } + /** Limit file size to 50 megabytes. */ + private static final int MAXIMUM_FILE_SIZE = 50 * 1024 * 1024; - public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException { - doPost(request, response); - } + /** How wide should a generated thumbnail image be (in pixels)? */ + private static final int THUMBNAIL_WIDTH = 150; - /* (non-Javadoc) - * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) - */ - /* (non-Javadoc) - * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) - */ - public void doPost(HttpServletRequest req, - HttpServletResponse response) - throws ServletException, IOException - { - - VitroRequest request = new VitroRequest(req); - - boolean overwriteExistingImage=false; - //BufferedReader in = null; - PrintWriter out = null; + /** How high should a generated thumbnail image be (in pixels)? */ + private static final int THUMBNAIL_HEIGHT = 150; - File sourceDir; // the working source directory for the website -- put the file here so if the site is refreshed the file won't be lost - File contentDir; // the actual website directory -- make a copy there so the user can see the uploaded file before the next ant deploy - File tempDir; // a temporary directory in the Tomcat context where files are uploaded before copying to sourceDir and websiteDir, and where reports are stored + private FileStorage fileStorage; - String paramStr = "
"; - String destinationStr=null; - String imageTypeStr=null; - String primaryContentTypeStr=null; - ArrayList secondaryContentTypeList=null; + private static Map createFileTypesMap() { + Map map = new HashMap(); + map.put(".gif", "image/gif"); + map.put(".png", "image/png"); + map.put(".jpg", "image/jpeg"); + map.put(".jpeg", "image/jpeg"); + map.put(".jpe", "image/jpeg"); + return Collections.unmodifiableMap(map); + } - String individualURI=null; + /** + * On startup, get a reference to the {@link FileStorage} system from the + * {@link ServletContext}. + * + * @throws UnavailableException + * if the attribute is missing, or is not of the correct type. + */ + @Override + public void init() throws ServletException { + Object o = getServletContext().getAttribute( + FileStorageSetup.ATTRIBUTE_NAME); + if (o instanceof FileStorage) { + fileStorage = (FileStorage) o; + } else if (o == null) { + throw new UnavailableException(this.getClass().getSimpleName() + + " could not initialize. Attribute '" + + FileStorageSetup.ATTRIBUTE_NAME + + "' was not set in the servlet context."); + } else { + throw new UnavailableException(this.getClass().getSimpleName() + + " could not initialize. Attribute '" + + FileStorageSetup.ATTRIBUTE_NAME + + "' in the servlet context contained an instance of '" + + o.getClass().getName() + "' instead of '" + + FileStorage.class.getName() + "'"); + } + } - String userName=null; - HttpSession session = request.getSession(); - LoginFormBean fb = (LoginFormBean) session.getAttribute("loginHandler"); - String tempDirName=null; - if ( fb != null ) { - userName = fb.getLoginName(); - tempDirName = websiteDirName + "/" + "batch"; - tempDir = new File(tempDirName); - if (!tempDir.exists() ) { - tempDir.mkdir(); - paramStr += "

Created new temporary working upload directory: " + tempDir.toString() + "

"; - } - tempDirName += "/" + userName; - tempDir = new File(tempDirName); - if (!tempDir.exists()) { - tempDir.mkdir(); - paramStr += "

Created new temporary working upload directory for user: " + userName + ": " + tempDir.toString() + "

"; - } - } else { - request.setAttribute("processError","User name not decoded from login formbean"); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } + /** + * Treat a GET request like a POST request. However, since a GET request + * cannot contain uploaded files, this should produce an error. + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doPost(request, response); + } - // Use an advanced form of the constructor that specifies a character encoding - // of the request (not of the file contents) and a file rename policy. - MultipartRequest multi = new MultipartRequest(request,tempDirName,10*1024*1024,"ISO-8859-1",new DefaultFileRenamePolicy()); + /** + *

+ * Store an image file as the main image for the associated individual. The + * request must be a multi-part request containing the file. It must also + * contain a parameter for the URI of the individual. + *

+ *

+ * If the individual already has a main image, it will be removed. The new + * image is stored, and a thumbnail is generated and stored. + *

+ */ + @Override + protected void doPost(HttpServletRequest rawRequest, + HttpServletResponse response) throws ServletException, IOException { + List errors = new ArrayList(); - String userStr="unknown"; // could get this from userName above - String remoteLocStr=null; + try { + VitroRequest request = new VitroRequest(FileUploadServletRequest + .parseRequest(rawRequest, MAXIMUM_FILE_SIZE)); + try { + FileItem imageFileItem = validateImageFromRequest(request); + Individual person = validateEntityUriFromRequest(request); - paramStr += "

PARAMS:

    "; - Enumeration params = multi.getParameterNames(); - while (params.hasMoreElements()) { - String name = (String)params.nextElement(); - if ( name.equalsIgnoreCase("entityUri")) { - individualURI = multi.getParameter(name); - paramStr += "
  • Individual URI = " + individualURI + "
  • "; - request.setAttribute("entityUri", individualURI ); - } else if ( name.equalsIgnoreCase("mode")) { - String modeStr = multi.getParameter(name); - overwriteExistingImage = modeStr.equalsIgnoreCase("replace") ? true : false; - paramStr += "
  • overwriting existing image = " + overwriteExistingImage + "
  • "; - } else if ( name.equalsIgnoreCase("submitter")) { - userStr = multi.getParameter(name); - //workDirName += "/" + userStr; - paramStr += "
  • user " + userStr + " storing in server directory = " + websiteDirName + "
  • "; - } else if ( name.equalsIgnoreCase("submitMode")) { - String submitStr = multi.getParameter(name); - paramStr += "
  • submitted via button: " + submitStr + "
  • "; - } else if ( name.equalsIgnoreCase("destination")) { - destinationStr = multi.getParameter(name); - paramStr += "
  • destination directory: " + destinationStr + "
  • "; - request.setAttribute("destination", destinationStr ); - } else if ( name.equalsIgnoreCase("type")) { - imageTypeStr = multi.getParameter(name); - paramStr += "
  • imageType: " + imageTypeStr + "
  • "; - request.setAttribute("type", imageTypeStr ); - } else if ( name.equalsIgnoreCase("contentType")) { - String contentTypeStr = multi.getParameter(name); - paramStr += "
  • acceptable content types: " + contentTypeStr + "
  • "; - StringTokenizer acceptedTypeTokens = new StringTokenizer( contentTypeStr,"/"); - int partCount = acceptedTypeTokens.countTokens(); - if ( partCount > 0 ) { - secondaryContentTypeList = new ArrayList(); - for (int i=0; i"; - } - } + FileModelHelper fileModelHelper = new FileModelHelper( + getWebappDaoFactory()); - try { - - sourceDir = null; - try { - sourceDir = new File(sourceDirName); - if (!sourceDir.exists()) { - sourceDir.mkdir(); - paramStr += "
  • Created new modifications directory in source area from which app is deployed: " + sourceDir.toString() + "
  • "; - } - sourceDir = new File(sourceDirName + "/images"); - if (!sourceDir.exists()) { - sourceDir.mkdir(); - paramStr += "
  • Created new image directory: " + sourceDir.toString() + "
  • "; - } - StringTokenizer uploadTokens = new StringTokenizer( destinationStr,"/"); - int uploadDepthCount = uploadTokens.countTokens(); - if ( uploadDepthCount > 0 ) { - for (int i=0; i"; - } - } - } - } catch (Exception e) { - log.warn("Unable to use source directory to back up uploaded image", e); - } + removeExistingImage(person, fileModelHelper); + storeMainImageFile(person, imageFileItem, fileModelHelper); + generateThumbnailAndStore(person, imageFileItem, + fileModelHelper); - String contentDirName = websiteDirName; - // check if top level output directory exists - contentDir = new File(contentDirName); - if (!contentDir.exists()) { - contentDir.mkdir(); - paramStr += "
  • Created new web site directory: " + contentDir.toString() + "
  • "; - } - contentDirName += "/" + "images"; - contentDir = new File( contentDirName ); - if (!contentDir.exists()) { - contentDir.mkdir(); - paramStr += "

    Created new website content directory " + contentDir.toString() + "

    "; - } - StringTokenizer outputTokens = new StringTokenizer( destinationStr,"/"); - int outputDepthCount = outputTokens.countTokens(); - if ( outputDepthCount > 0 ) { - for (int i=0; i"; - } - } - } - } catch (Exception ex) { - log.error("Exception when creating directories ", ex); - request.setAttribute("processError","Upload failed: unable to create directories for uploads. See error log for details."); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } + displaySuccess(request, response, person); + } catch (UserErrorException e) { + // No need to log it - it's a user error. + errors.add(e.getMessage()); + displayFailure(request, response, errors); + } catch (IllegalStateException e) { + log.error(e); + errors.add(e.getMessage()); + displayFailure(request, response, errors); + } + } catch (FileUploadException e) { + log.error(e); + errors.add(e.getMessage()); + displayFailure(rawRequest, response, errors); + } + } - paramStr += "

"; - request.setAttribute("prevparams", paramStr ); + /** + * The image must be present and non-empty, and must have a mime-type that + * represents an image we support. + * + * We rely on the fact that a {@link FileUploadServletRequest} will always + * have a map of {@link FileItem}s, even if it is empty. However, that map + * may not contain the field that we want, or that field may contain an + * empty file. + * + * @throws UserErrorException + * if there is no file, if it is empty, or if it is not an image + * file. + */ + @SuppressWarnings("unchecked") + private FileItem validateImageFromRequest(HttpServletRequest request) + throws UserErrorException { + Map> map = (Map>) request + .getAttribute(FileUploadServletRequest.FILE_ITEM_MAP); + List list = map.get(FILE_FIELD_NAME); + if ((list == null) || list.isEmpty()) { + throw new UserErrorException("The form did not contain a '" + + FILE_FIELD_NAME + "' field."); + } - String originalFileName = null; - String infileFullPathName = null; - String filesystemName = null; + FileItem file = list.get(0); + if (file.getSize() == 0) { + throw new UserErrorException("No file was uploaded in '" + + FILE_FIELD_NAME + "'"); + } - Enumeration files = multi.getFileNames(); - if (files.hasMoreElements()) { - String thisInputName = (String)files.nextElement(); - filesystemName = multi.getFilesystemName(thisInputName); // file name after any renaming, e.g., if file by that name already exists - originalFileName = multi.getOriginalFileName(thisInputName); // before renaming policy applied - if (thisInputName == null || thisInputName.equals("")) { - log.error("No input file provided for upload"); - request.setAttribute("processError","Error: no input file provided for upload"); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } + String filename = getSimpleFilename(file); + String mimeType = getMimeType(file); + if (!RECOGNIZED_FILE_TYPES.containsValue(mimeType)) { + throw new UserErrorException("'" + filename + + "' is not a recognized image file type. " + + "These are the recognized types: " + + RECOGNIZED_FILE_TYPES); + } - String typeStr = multi.getContentType(thisInputName); - if ( typeStr==null || typeStr.equals("")) { - log.error("Error: if input file provided, it has no readable file type"); - request.setAttribute("processError","Error: if an input file was provided, it has no readable file type"); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } + return file; + } - StringTokenizer contentTypeTokens = new StringTokenizer( typeStr,"/"); - int typeTokenCount = contentTypeTokens.countTokens(); - if ( typeTokenCount == 2 ) { - for (int i=0; i 0) { - Iterator typeIter = secondaryContentTypeList.iterator(); - boolean typeMatch=false; - String typeConcat=primaryContentTypeStr; - int count=0; - while ( typeIter.hasNext() ) { - String whichType = (String)typeIter.next(); - if ( count == 0 ) { - typeConcat="/" + whichType; - } else { - typeConcat+=" or " + primaryContentTypeStr + "/" + whichType; - } - ++count; - if (whichType.equals("*") || whichType.equalsIgnoreCase(partStr)) { - typeMatch=true; - } - } - if (!typeMatch) { - log.error("Error: file uploaded (" + originalFileName + ") has content type " + typeStr + " that does not match '" + primaryContentTypeStr + typeConcat + "'"); - request.setAttribute("processError","Error: file uploaded (" + originalFileName + ") has content type " + typeStr + " that does not match " + primaryContentTypeStr + typeConcat ); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } - } // else any secondary type accepted - break; - } - } - } else { - log.error("Error: file uploaded (" + originalFileName + ") has unrecognized content type '" + typeStr + "'"); - request.setAttribute("processError","Error: file uploaded (" + originalFileName + ") has unrecognized content type '" + typeStr + "'"); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } + /** + * The entity URI must be present and non-empty, and must refer to an + * existing individual. + * + * @throws UserErrorException + * if there is no entity URI, or if it is empty. + */ + private Individual validateEntityUriFromRequest(VitroRequest request) + throws UserErrorException { + String entityUri = request.getParameter(URI_FIELD_NAME); + if (entityUri == null) { + throw new UserErrorException("The form did not contain a '" + + URI_FIELD_NAME + "' field."); + } + entityUri = entityUri.trim(); + if (entityUri.length() == 0) { + throw new UserErrorException("The form did not contain a '" + + URI_FIELD_NAME + "' field."); + } - File f = multi.getFile(thisInputName); // see Core Java v1 pp 769 onward on File managment - if (f != null) { - infileFullPathName = f.toString(); - } - } + Individual entity = request.getWebappDaoFactory().getIndividualDao() + .getIndividualByURI(entityUri); + if (entity == null) { + throw new UserErrorException( + "No entity exists with the provided URI: '" + entityUri + + "'"); + } + return entity; + } - request.setAttribute("input",originalFileName); // but can't specify value parameter for form inputs of type file + /** + * If this entity already had a main image, remove the connection. If the + * image and the thumbnail are no longer used by anyone, remove them from + * the model, and from the file system. + */ + private void removeExistingImage(Individual person, + FileModelHelper fileModelHelper) { + Individual mainImage = fileModelHelper.removeMainImage(person); + if (mainImage == null) { + return; + } - int posDot = originalFileName.lastIndexOf('.'); //filesystemName.lastIndexOf('.') to increment versions; - try { - // BufferedReader handles the input file like a TEXT file (you can read lines from it) - // BufferedInputStream handles the input file like a BINARY file (if you want to read a line from it you must read - // it character by character until finding the line separator) - // They are meant to do DIFFERENT things -- use the class that is more suitable for your task - // For uploading text and parsing it use: BufferedReader in = new BufferedReader(new FileReader(infileFullPathName)); - out= new PrintWriter( new FileWriter( tempDir + "/" + originalFileName.substring(0,posDot) + ".html")); - request.setAttribute("outputLink",""+originalFileName.substring(0,posDot)+".html"); + Individual thumbnail = FileModelHelper.getThumbnailForImage(mainImage); - } catch ( IOException ex ) { - request.setAttribute("processError","error creating report file " + tempDir + "/" + originalFileName.substring(0,posDot) + ".html: " + ex.getMessage()); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } + if (!fileModelHelper.isFileReferenced(mainImage)) { + Individual bytes = FileModelHelper.getBytestreamForFile(mainImage); + if (bytes != null) { + try { + fileStorage.deleteFile(bytes.getURI()); + } catch (IOException e) { + throw new IllegalStateException( + "Can't delete the main image file: '" + + bytes.getURI() + "'", e); + } + } + fileModelHelper.removeFileFromModel(mainImage); + } + if (!fileModelHelper.isFileReferenced(thumbnail)) { + Individual bytes = FileModelHelper.getBytestreamForFile(thumbnail); + if (bytes != null) { + try { + fileStorage.deleteFile(bytes.getURI()); + } catch (IOException e) { + throw new IllegalStateException( + "Can't delete the thumbnail file: '" + + bytes.getURI() + "'", e); + } + } + fileModelHelper.removeFileFromModel(thumbnail); + } + } - out.println(""); - out.println(""); - out.println("Upload Report"); - out.println(""); - out.println(""); - out.println(""); - out.println("

Image Upload Report

"); + /** + * Store this image in the model and in the file storage system, and set it + * as the main image for this person. + */ + private void storeMainImageFile(Individual person, FileItem imageFileItem, + FileModelHelper fileModelHelper) { + InputStream inputStream = null; + try { + inputStream = imageFileItem.getInputStream(); + String mimeType = getMimeType(imageFileItem); + String filename = getSimpleFilename(imageFileItem); - out.println( loadImage(request,originalFileName,overwriteExistingImage,destinationStr,imageTypeStr,individualURI,remoteLocStr)); + // Create the file individuals in the model + Individual byteStream = fileModelHelper + .createByteStreamIndividual(); + Individual file = fileModelHelper.createFileIndividual(mimeType, + filename, byteStream); - String contextName = request.getContextPath(); // e.g., /vivo - log.info("context name from getContextPath(): " + contextName); + // Store the file in the FileStorage system. + fileStorage.createFile(byteStream.getURI(), filename, inputStream); - out.println("

" + originalFileName + "

"); - out.println("

" + originalFileName + "

"); - out.println("

" + originalFileName + "

"); + // Set the file as the main image for the person. + fileModelHelper.setAsMainImageOnEntity(person, file); + } catch (FileAlreadyExistsException e) { + throw new IllegalStateException( + "Can't create the main image file: " + e.getMessage(), e); + } catch (IOException e) { + throw new IllegalStateException("Can't create the main image file", + e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } - // Actually open the input file for copying - BufferedInputStream input = new BufferedInputStream(new FileInputStream(infileFullPathName)); + /** + * Generate the thumbnail image from the original, store in the model and in + * the file storage system, and set it as the thumbnail on the main image. + */ + private void generateThumbnailAndStore(Individual person, + FileItem imageFileItem, FileModelHelper fileModelHelper) { + InputStream inputStream = null; + try { + inputStream = scaleImageForThumbnail( + imageFileItem.getInputStream(), THUMBNAIL_WIDTH, + THUMBNAIL_HEIGHT); + String mimeType = RECOGNIZED_FILE_TYPES.get(".jpg"); + String filename = createThumbnailFilename(getSimpleFilename(imageFileItem)); - // Create the copies of the uploaded file - FileOutputStream sourceFile = null; // The copy of the uploaded file for the working directory - FileOutputStream contentFile = null; // the copy put directly on the web site so the user can see it before next ant deploy is done - if (sourceDir != null) { - try { - sourceFile = new FileOutputStream(sourceDir + File.separator + originalFileName); // fileSystemName to increment versions); // apparently don't need File.separator - } catch (FileNotFoundException fnf ) { - request.setAttribute("processError","Warning: could not create image backup file (" + sourceDir + File.separator + originalFileName); - } - } else { - String msg = "

Warning: unable to make a backup copy of uploaded image.

"; - Object processErrorAttribute = request.getAttribute("processError"); - if ( (processErrorAttribute != null) && (processErrorAttribute instanceof String) ) { - request.setAttribute("processError",((String)processErrorAttribute)+msg); - } else { - request.setAttribute("processError",msg); - } - } - try { - contentFile = new FileOutputStream(contentDir + File.separator + originalFileName); // fileSystemName to increment versions); - } catch (FileNotFoundException fnf) { - out.println("Error: the image file cannot be created
"); - out.println(fnf.getMessage() + "
"); - request.setAttribute("processError","Error: could not create image file (" + contentDir + File.separator + originalFileName); - getServletConfig().getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( request, response ); - return; - } + // Create the file individuals in the model + Individual byteStream = fileModelHelper + .createByteStreamIndividual(); + Individual file = fileModelHelper.createFileIndividual(mimeType, + filename, byteStream); + // Store the file in the FileStorage system. + fileStorage.createFile(byteStream.getURI(), filename, inputStream); - /* Read from the Input Stream and write into the File OutputStream */ - int length = 1000; - byte[] byteArray = new byte[1000]; - try { - length = input.read( byteArray ); - while (length != -1) { - if (sourceFile != null) - sourceFile.write( byteArray, 0, length); - contentFile.write( byteArray, 0, length); - length = input.read( byteArray ); - } - } catch (IOException ioe) { - out.println("The input file does not seem to be readable (IO Error):
"); - out.println( ioe.getMessage() + "
"); - } catch (Exception e) { - out.println("
The Following Exception occured in the servlet:
"); - out.println("
" + e.toString() + "
"); - } + // Set the file as the thumbnail on the main image for the person. + fileModelHelper.setThumbnailOnIndividual(person, file); + } catch (FileAlreadyExistsException e) { + throw new IllegalStateException("Can't create the thumbnail file: " + + e.getMessage(), e); + } catch (IOException e) { + throw new IllegalStateException("Can't create the thumbnail file", + e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } - if (sourceFile != null) - sourceFile.close(); - contentFile.close(); + /** + * Everything went fine. Forward back to the JSP. + */ + private void displaySuccess(VitroRequest request, + HttpServletResponse response, Individual person) + throws ServletException, IOException { + try { + request.setAttribute(URI_FIELD_NAME, person.getURI()); - try { // now delete input file from temp directory - out.println("

opening " + infileFullPathName + " for deletion

"); - File tempFile = new File( infileFullPathName ); - if ( tempFile.exists()) { - try { - boolean gone = tempFile.delete(); - if ( gone ) { - out.println("

deleted file " + infileFullPathName + " since has been copied to web site

"); - } else { - out.println("

could not delete file " + infileFullPathName + "

"); - } - } catch ( Exception ex ) { - out.println("

Exception: " + ex.getMessage() + "
"); - ex.printStackTrace (); - out.println("

"); - } - } else { - out.println("

Error -- file " + infileFullPathName + " does not exist

"); - } - } catch (Exception ex) { - out.println("

Exception: " + ex.getMessage() + "
"); - ex.printStackTrace (); - out.println("

"); - } + String individualURI = person.getURI(); + String recordName = person.getName(); + if ((recordName == null) || recordName.isEmpty()) { + recordName = "here"; + } + request.setAttribute("processError", + "updated individual " + + recordName + ""); + request.setAttribute("outputLink", ""); + getServletContext().getRequestDispatcher("/uploadimages.jsp") + .forward(request, response); + } catch (UnsupportedEncodingException e) { + log.error("This can't happen.", e); + } + } - out.println("
"); - out.println(""); - out.flush(); - out.close(); + /** + * Problem! Format the error messages and forward back to the JSP. + */ + private void displayFailure(HttpServletRequest request, + HttpServletResponse response, List errors) + throws ServletException, IOException { + String entityUri = request.getParameter(URI_FIELD_NAME); + request.setAttribute(URI_FIELD_NAME, entityUri); - getServletContext().getRequestDispatcher( "/uploadimages.jsp" ).forward( request, response ); - } + StringBuilder formatted = new StringBuilder(); + for (String error : errors) { + if (formatted.length() > 0) { + formatted.append("
"); + } + formatted.append(error); + } + request.setAttribute("processError", formatted.toString()); + getServletContext().getRequestDispatcher("/uploadimages.jsp").forward( + request, response); + } - public String loadImage(HttpServletRequest request,String fileName,boolean overwriteExisting,String destination,String imageType,String individualURI,String optionalRemoteLocStr) - { - String messageStr="

"; + /** + * Internet Explorer and Opera will give you the full path along with the + * filename. This will remove the path. + */ + private String getSimpleFilename(FileItem item) { + String fileName = item.getName(); + if (fileName == null) { + return null; + } else { + return FilenameUtils.getName(fileName); + } + } - // first check to verify that individual exists . - Individual individual = getWebappDaoFactory().getIndividualDao().getIndividualByURI(individualURI); - String recordName = null, previousImageStr=null; - try { - if (individual != null) { - recordName = individual.getName(); - previousImageStr = individual.getImageThumb(); - messageStr += "

Uploading file for individual: " + recordName + "

"; - } else { - log.error("Error: no individual found with URI " + individualURI); - request.setAttribute("processError","Error: no individual found with URI " + individualURI); - messageStr += "Error: no individual found with URI " + individualURI + "

"; - return messageStr; - } - } catch ( Exception ex ) { - log.error("Error: exception on checking individual URI " + individualURI + ": " + ex.getMessage()); - request.setAttribute("processError","Error: exception on checking individual URI " + individualURI + ": " + ex.getMessage()); - return messageStr; - } + /** + * Get the MIME type as supplied by the browser. If none, try to infer it + * from the filename extension and the map of recognized MIME types. + */ + private String getMimeType(FileItem file) { + String mimeType = file.getContentType(); + if (mimeType != null) { + return mimeType; + } - boolean individualUpdated=false; - boolean noExistingImage=(previousImageStr==null || previousImageStr.equals(""))? true : false; - if (noExistingImage || overwriteExisting) { - if (imageType.equalsIgnoreCase("thumb")) { - if (optionalRemoteLocStr!=null && !optionalRemoteLocStr.equals("") && !optionalRemoteLocStr.equals("http://")) { - individual.setImageFile(optionalRemoteLocStr); - individual.setImageThumb(destination+"/"+fileName); - } else { - individual.setImageThumb(destination+"/"+fileName); - } - } else { - individual.setImageFile(destination+"/"+fileName); - } - try { - getWebappDaoFactory().getIndividualDao().updateIndividual(individual); - individualUpdated=true; - } catch ( Exception ex ) { - log.error("Error: Exception on getWebappDaoFactory().getIndividualDao().updateIndividual(" + individualURI +"); message: " + ex.getMessage()); - request.setAttribute("processError","Error: Exception on getWebappDaoFactory().getIndividualDao().updateIndividual(" + individualURI +"); message: " + ex.getMessage()); - return messageStr; - } - } else if (!noExistingImage) { - if (imageType.equalsIgnoreCase("thumb")) { - messageStr += "

This individual already has a thumbnail image associated with it: " + previousImageStr + "

"; - } else { - messageStr += "

This individual already has an optional large-size image associated with it: " + previousImageStr + "

"; - } - } + String filename = getSimpleFilename(file); + int periodHere = filename.lastIndexOf('.'); + if (periodHere == -1) { + return null; + } - messageStr += ""; - messageStr += ""; - messageStr += ""; - messageStr += ""; - messageStr += ""; - messageStr += ""; - messageStr += "
individual idimage
" + individualURI + "" + ((fileName == null || fileName.equals("")) ? "missing image file name" : fileName ) + "
"; + String extension = filename.substring(periodHere); + return RECOGNIZED_FILE_TYPES.get(extension); + } - try { - if (individualUpdated) { - request.setAttribute("processError","updated individual "+recordName+""); - } else { - request.setAttribute("processError","individual "+recordName+" already has an image: please confirm if you wish to replace it"); - } - } catch (UnsupportedEncodingException ex) { - request.setAttribute("processError","Could not create link to individual "+recordName+" (URI: "+individualURI+")"); - } + private InputStream scaleImageForThumbnail(InputStream source, int width, + int height) throws IOException { + BufferedImage bsrc = ImageIO.read(source); - return messageStr; - } + AffineTransform at = AffineTransform.getScaleInstance((double) width + / bsrc.getWidth(), (double) height / bsrc.getHeight()); - /** - * attempts to get the property file from UPLOAD_SERVLET_PROPERTIES - * and returns the property UploadImagesServlet.sourceDirName. - * @return returns the property UploadImagesServlet.sourceDirName - * @throws IOException - */ - private String getSourceDirName() throws IOException{ - String dirName = ConfigurationProperties.getProperty("upload.directory"); - if( dirName == null ) { - log.error("getSourceDirName(): property sourceDirName not defined in configuration properties"); - } else { - File dir = new File(dirName); - if(!dir.exists()) { - log.warn("getSourceDirName(): " + - "The specified upload directory "+ dirName + " does not exist. " + - "Not saving upload images to source dir."); - } - if(!dir.canWrite()) { - log.warn("getSourceDirName(): " + - "The specified upload directory "+ dirName + " is not writable." + - " Not saving upload images to source dir."); - } - } - return dirName; - } + BufferedImage bdest = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = bdest.createGraphics(); - private static String stripDoubleQuotes( String termStr ) { - if (termStr==null || termStr.equals("")) { - return termStr; - } - int whichChar = 32; //double quote - int characterPosition= -1; - while ( ( characterPosition = termStr.indexOf( whichChar, characterPosition+1 ) ) >= 0 ) { - termStr = termStr.substring( characterPosition+1 ); - ++characterPosition; - } - return termStr; - } + g.drawRenderedImage(bsrc, at); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ImageIO.write(bdest, "JPG", buffer); + return new ByteArrayInputStream(buffer.toByteArray()); + } - private static String escapeSingleQuotes( String termStr ) { - if (termStr==null || termStr.equals("")) { - return termStr; - } - int whichChar = 39; //single quote - int characterPosition= -1; - while ( ( characterPosition = termStr.indexOf( whichChar, characterPosition+1 ) ) >= 0 ) { - if ( characterPosition == 0 ) // just drop it - termStr = termStr.substring( characterPosition+1 ); - else if ( termStr.charAt(characterPosition-1) != 92 ) // already escaped - termStr = termStr.substring(0,characterPosition) + "\\" + termStr.substring(characterPosition); - ++characterPosition; - } - return termStr; - } + /** + * Create a name for the thumbnail from the name of the original file. + * "myPicture.anything" becomes "thumbnail_myPicture.jpg". + */ + private String createThumbnailFilename(String filename) { + String prefix = "thumbnail_"; + String extension = ".jpg"; + int periodHere = filename.lastIndexOf('.'); + if (periodHere == -1) { + return prefix + filename + extension; + } else { + return prefix + filename.substring(0, periodHere) + extension; + } + } - - private static String stripLeadingAndTrailingSpaces( String termStr ) { - int characterPosition= -1; - - while ( ( characterPosition = termStr.indexOf( 32, characterPosition+1 ) ) == 0 ) { - termStr = termStr.substring(characterPosition+1); - } - while ( termStr.indexOf(32) >= (termStr.length()-1) ) { - termStr = termStr.substring(0,termStr.length()-1); - } - return termStr; - } + /** + * Indicates that we should complain to the user. + */ + private static class UserErrorException extends Exception { + UserErrorException(String message) { + super(message); + } + } } - - - diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/IndividualController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/IndividualController.java index 9d3235f22..9aa2039fe 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/IndividualController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/IndividualController.java @@ -43,6 +43,10 @@ import edu.cornell.mannlib.vitro.webapp.controller.Controllers; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileServingHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; import edu.cornell.mannlib.vitro.webapp.search.beans.VitroQuery; import edu.cornell.mannlib.vitro.webapp.search.beans.VitroQueryWrapper; import edu.cornell.mannlib.vitro.webapp.utils.NamespaceMapper; @@ -109,6 +113,13 @@ public class IndividualController extends FreeMarkerHttpServlet { return; } + // If this is an uploaded file, redirect to its "alias URL". + String aliasUrl = getAliasUrlForBytestreamIndividual(indiv); + if (aliasUrl != null) { + res.sendRedirect(req.getContextPath() + aliasUrl); + return; + } + doHtml( vreq, res , indiv); return; @@ -488,6 +499,47 @@ public class IndividualController extends FreeMarkerHttpServlet { return false; } + /** + * If this entity represents a File Bytestream, get its alias URL so we can + * properly serve the file contents. + */ + private String getAliasUrlForBytestreamIndividual(Individual entity) + throws IOException { + if (!FileModelHelper.isFileBytestream(entity)) { + log.debug("Entity at '" + entity.getURI() + + "' is not recognized as a FileByteStream."); + return null; + } + + FileStorage fs = (FileStorage) getServletContext().getAttribute( + FileStorageSetup.ATTRIBUTE_NAME); + if (fs == null) { + log.error("Servlet context does not contain file storage at '" + + FileStorageSetup.ATTRIBUTE_NAME + "'"); + return null; + } + + String filename = fs.getFilename(entity.getURI()); + if (filename == null) { + log.error("Entity at '" + entity.getURI() + + "' is recognized as a FileByteStream, " + + "but the file system does not recognize it."); + return null; + } + + String url = FileServingHelper.getBytestreamAliasUrl(entity.getURI(), + filename); + if (url.equals(entity.getURI())) { + log.error("Entity at '" + entity.getURI() + + "' is recognized as a FileByteStream, " + + "but can't be translated to an alias URL."); + return null; + } + + log.debug("Alias URL for '" + entity.getURI() + "' is '" + url + "'"); + return url; + } + private Model getRDF(Individual entity, OntModel contextModel, Model newModel, int recurseDepth ) { Resource subj = newModel.getResource(entity.getURI()); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java index 2b3f35c79..50e9b33fb 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java @@ -2,18 +2,14 @@ package edu.cornell.mannlib.vitro.webapp.dao; -import java.util.Arrays; -import java.util.List; - -import com.hp.hpl.jena.rdf.model.Resource; -import com.hp.hpl.jena.vocabulary.XSD; -import com.hp.hpl.jena.ontology.AnnotationProperty; public class VitroVocabulary { public static final String vitroURI = "http://vitro.mannlib.cornell.edu/ns/vitro/0.7#"; + public static final String VITRO_PUBLIC = "http://vitro.mannlib.cornell.edu/ns/vitro/public#"; + /** BJL23 2008-02-25: * This is a hack. The classic Vitro code is heavily reliant on simple identifiers, and it will take some doing to completely @@ -91,8 +87,6 @@ public class VitroVocabulary { public static final String DISPLAY_LIMIT = vitroURI+"displayLimitAnnot"; public static final String CITATION = vitroURI+"citation"; - public static final String IMAGEFILE = vitroURI+"imageFile"; - public static final String IMAGETHUMB = vitroURI+"imageThumb"; // ================== property related ================================= @@ -284,5 +278,18 @@ public class VitroVocabulary { public static final String ONTOLOGY_PREFIX_ANNOT = vitroURI + "ontologyPrefixAnnot"; + // =============== file storage vocabulary ================================ + + public static final String FS_FILE_CLASS = VITRO_PUBLIC + "File"; + public static final String FS_BYTESTREAM_CLASS = VITRO_PUBLIC + "FileByteStream"; + + public static final String FS_FILENAME = VITRO_PUBLIC + "filename"; + public static final String FS_MIME_TYPE = VITRO_PUBLIC + "mimeType"; + public static final String FS_ATTRIBUTION = VITRO_PUBLIC + "attribution"; + public static final String FS_DOWNLOAD_LOCATION = VITRO_PUBLIC + "downloadLocation"; + public static final String FS_THUMBNAIL_IMAGE = VITRO_PUBLIC + "thumbnailImage"; + + public static final String IND_MAIN_IMAGE = VITRO_PUBLIC + "mainImage"; + public static final String IND_IMAGE = VITRO_PUBLIC + "image"; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/IndividualFiltering.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/IndividualFiltering.java index 8b1c0eb4d..257a77d7a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/IndividualFiltering.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/IndividualFiltering.java @@ -242,18 +242,24 @@ public class IndividualFiltering implements Individual { return _innerIndividual.getFlag3Set(); } + @Override + public String getMainImageUri() { + return _innerIndividual.getMainImageUri(); + } - public String getImageFile() { - return _innerIndividual.getImageFile(); - } + @Override + public String getImageUrl() { + return _innerIndividual.getImageUrl(); + } - public String getImageThumb() { - return _innerIndividual.getImageThumb(); - } + @Override + public String getThumbUrl() { + return _innerIndividual.getThumbUrl(); + } - public List getKeywords() { + public List getKeywords() { return _innerIndividual.getKeywords(); } @@ -405,15 +411,10 @@ public class IndividualFiltering implements Individual { } - public void setImageFile(String imageFile) { - _innerIndividual.setImageFile(imageFile); - } - - - public void setImageThumb(String imageThumb) { - _innerIndividual.setImageThumb(imageThumb); - } - + @Override + public void setMainImageUri(String mainImageUri) { + _innerIndividual.setMainImageUri(mainImageUri); + } public void setKeywords(List keywords) { _innerIndividual.setKeywords(keywords); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualDaoJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualDaoJena.java index 9967452e6..142549d65 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualDaoJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualDaoJena.java @@ -21,7 +21,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.joda.time.DateTime; -import com.hp.hpl.jena.ontology.DatatypeProperty; import com.hp.hpl.jena.ontology.OntClass; import com.hp.hpl.jena.ontology.OntModel; import com.hp.hpl.jena.ontology.OntResource; @@ -56,7 +55,6 @@ import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; import edu.cornell.mannlib.vitro.webapp.dao.InsertException; import edu.cornell.mannlib.vitro.webapp.dao.KeywordDao; import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; -import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.IndividualCreationEvent; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.IndividualDeletionEvent; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.IndividualUpdateEvent; @@ -313,8 +311,10 @@ public class IndividualDaoJena extends JenaBaseDao implements IndividualDao { addPropertyDateTimeValue(ind,SUNSET,ent.getSunset(), ontModel); addPropertyDateTimeValue(ind,TIMEKEY,ent.getTimekey(), ontModel); addPropertyDateTimeValue(ind,MODTIME,Calendar.getInstance().getTime(),ontModel); - addPropertyStringValue(ind,IMAGETHUMB,ent.getImageThumb(),ontModel); - addPropertyStringValue(ind,IMAGEFILE,ent.getImageFile(),ontModel); + if (ent.getMainImageUri() != null) { + addPropertyResourceURIValue(ind, IND_MAIN_IMAGE, ent.getMainImageUri()); + } + if (ent.getAnchor()!= null && ent.getAnchor().length()>0 && LINK != null) { com.hp.hpl.jena.ontology.Individual primaryLink = ontModel.createIndividual(entURI+"_primaryLink", LINK); primaryLink.addProperty(RDF.type, LINK); @@ -379,8 +379,9 @@ public class IndividualDaoJena extends JenaBaseDao implements IndividualDao { ent.getFlag1Numeric(); ent.getFlag1Set(); ent.getFlag2Set(); - ent.getImageFile(); - ent.getImageThumb(); + ent.getMainImageUri(); + ent.getImageUrl(); + ent.getThumbUrl(); ent.getKeywords(); ent.getKeywordString(); ent.getLinksList(); @@ -472,9 +473,8 @@ public class IndividualDaoJena extends JenaBaseDao implements IndividualDao { updatePropertyDateTimeValue(ind,SUNRISE,ent.getSunrise(), ontModel); updatePropertyDateTimeValue(ind,SUNSET,ent.getSunset(), ontModel); updatePropertyDateTimeValue(ind,TIMEKEY,ent.getTimekey(), ontModel); - updatePropertyStringValue(ind,IMAGETHUMB,ent.getImageThumb(),ontModel); - updatePropertyStringValue(ind,IMAGEFILE,ent.getImageFile(),ontModel); updatePropertyDateTimeValue(ind,MODTIME,Calendar.getInstance().getTime(),ontModel); + updatePropertyResourceURIValue(ind, IND_MAIN_IMAGE, ent.getMainImageUri(), ontModel); if (ent.getAnchor()!= null && ent.getAnchor().length()>0) { if (LINK != null && PRIMARY_LINK != null) { boolean updatedExisting = false; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualJena.java index 5e9e6ce83..a32d3823a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/IndividualJena.java @@ -40,6 +40,8 @@ import edu.cornell.mannlib.vitro.webapp.beans.ObjectProperty; import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement; import edu.cornell.mannlib.vitro.webapp.beans.VClass; import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileServingHelper; import edu.cornell.mannlib.vitro.webapp.utils.FlagMathUtils; public class IndividualJena extends IndividualImpl implements Individual { @@ -474,33 +476,54 @@ public class IndividualJena extends IndividualImpl implements Individual { } } - public String getImageFile() { - if (this.imageFile != null) { - return imageFile; - } else { - ind.getOntModel().enterCriticalSection(Lock.READ); - try { - imageFile = webappDaoFactory.getJenaBaseDao().getPropertyStringValue(ind,webappDaoFactory.getJenaBaseDao().IMAGEFILE); - return imageFile; - } finally { - ind.getOntModel().leaveCriticalSection(); - } - } - } + @Override + public String getMainImageUri() { + if (this.mainImageUri != NOT_INITIALIZED) { + return mainImageUri; + } else { + for (ObjectPropertyStatement stmt : getObjectPropertyStatements()) { + if (stmt.getPropertyURI() + .equals(VitroVocabulary.IND_MAIN_IMAGE)) { + mainImageUri = stmt.getObjectURI(); + return mainImageUri; + } + } + return null; + } + } - public String getImageThumb() { - if (this.imageThumb != null) { - return imageThumb; - } else { - ind.getOntModel().enterCriticalSection(Lock.READ); - try { - imageThumb = webappDaoFactory.getJenaBaseDao().getPropertyStringValue(ind,webappDaoFactory.getJenaBaseDao().IMAGETHUMB); - return imageThumb; - } finally { - ind.getOntModel().leaveCriticalSection(); - } - } - } + @Override + public String getImageUrl() { + if (this.imageUrl != null) { + log.debug("imageUrl was cached for " + getURI() + ": '" + + this.imageUrl + "'"); + return imageUrl; + } else { + String imageUri = FileModelHelper.getMainImageBytestreamUri(this); + String filename = FileModelHelper.getMainImageFilename(this); + imageUrl = FileServingHelper.getBytestreamAliasUrl(imageUri, + filename); + log.debug("figured imageUrl for " + getURI() + ": '" + + this.imageUrl + "'"); + return imageUrl; + } + } + + @Override + public String getThumbUrl() { + if (this.thumbUrl != null) { + log.debug("thumbUrl was cached for " + getURI() + ": '" + + this.thumbUrl + "'"); + return thumbUrl; + } else { + String imageUri = FileModelHelper.getThumbnailBytestreamUri(this); + String filename = FileModelHelper.getThumbnailFilename(this); + thumbUrl = FileServingHelper.getBytestreamAliasUrl(imageUri, filename); + log.debug("figured thumbUrl for " + getURI() + ": '" + + this.thumbUrl + "'"); + return thumbUrl; + } + } public String getAnchor() { if (this.anchor != null) { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java index 75f698dfe..8a97d9b12 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java @@ -60,8 +60,6 @@ public class JenaBaseDaoCon { protected DatatypeProperty MODTIME = _constModel.createDatatypeProperty(VitroVocabulary.MODTIME); protected DatatypeProperty TIMEKEY = _constModel.createDatatypeProperty(VitroVocabulary.TIMEKEY); protected DatatypeProperty CITATION = _constModel.createDatatypeProperty(VitroVocabulary.CITATION); - protected DatatypeProperty IMAGETHUMB = _constModel.createDatatypeProperty(VitroVocabulary.IMAGETHUMB); - protected DatatypeProperty IMAGEFILE = _constModel.createDatatypeProperty(VitroVocabulary.IMAGEFILE); protected DatatypeProperty DISPLAY_RANK = _constModel.createDatatypeProperty(VitroVocabulary.DISPLAY_RANK); protected AnnotationProperty DISPLAY_RANK_ANNOT = _constModel.createAnnotationProperty(VitroVocabulary.DISPLAY_RANK_ANNOT); @@ -183,6 +181,17 @@ public class JenaBaseDaoCon { protected AnnotationProperty ONTOLOGY_PREFIX_ANNOT = _constModel.createAnnotationProperty(VitroVocabulary.ONTOLOGY_PREFIX_ANNOT); + protected OntClass FS_FILE = _constModel.createClass(VitroVocabulary.FS_FILE_CLASS); + protected OntClass FS_BYTESTREAM = _constModel.createClass(VitroVocabulary.FS_BYTESTREAM_CLASS); + protected ObjectProperty FS_DOWNLOAD_LOCATION = _constModel.createObjectProperty(VitroVocabulary.FS_DOWNLOAD_LOCATION); + protected ObjectProperty FS_THUMBNAIL_IMAGE = _constModel.createObjectProperty(VitroVocabulary.FS_THUMBNAIL_IMAGE); + protected DatatypeProperty FS_FILENAME = _constModel.createDatatypeProperty(VitroVocabulary.FS_FILENAME); + protected DatatypeProperty FS_MIME_TYPE = _constModel.createDatatypeProperty(VitroVocabulary.FS_MIME_TYPE); + protected DatatypeProperty FS_ATTRIBUTION = _constModel.createDatatypeProperty(VitroVocabulary.FS_ATTRIBUTION); + + protected ObjectProperty IND_MAIN_IMAGE = _constModel.createObjectProperty(VitroVocabulary.IND_MAIN_IMAGE); + protected ObjectProperty IND_IMAGE = _constModel.createObjectProperty(VitroVocabulary.IND_IMAGE); + public OntModel getConstModel() { return _constModel; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/tabFactory/TabEntityFactoryGalleryJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/tabFactory/TabEntityFactoryGalleryJena.java index 062053a5b..8e7f4e7ba 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/tabFactory/TabEntityFactoryGalleryJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/tabFactory/TabEntityFactoryGalleryJena.java @@ -36,7 +36,7 @@ public class TabEntityFactoryGalleryJena extends TabEntityFactoryJena implements TabEntityFactory { private TabEntityFactory _innerFactory = null; - public final UnaryFunctoronlyWithThumbs = new OnlyWithThumbs(); + public final UnaryFunctoronlyWithMainImage = new OnlyWithMainImage(); public TabEntityFactoryGalleryJena(TabEntityFactory innerEntFactory, Tab tab, int auth_level, ApplicationBean appBean, WebappDaoFactoryJena wadf) { super(tab, auth_level, appBean, wadf); @@ -79,7 +79,7 @@ public class TabEntityFactoryGalleryJena extends TabEntityFactoryJena return Collections.EMPTY_LIST; List filteredEnts = new LinkedList( ); - Filter.filter(ents,onlyWithThumbs,filteredEnts); + Filter.filter(ents,onlyWithMainImage,filteredEnts); if( filteredEnts.size() <= numberOfrequestedEnts) return filteredEnts; @@ -94,10 +94,10 @@ public class TabEntityFactoryGalleryJena extends TabEntityFactoryJena return entsOut; } - private class OnlyWithThumbs extends UnaryFunctor{ + private class OnlyWithMainImage extends UnaryFunctor{ @Override public Boolean fn(Individual arg) { - return arg.getImageThumb() != null; + return arg.getMainImageUri()!= null; } } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/FileModelHelper.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/FileModelHelper.java new file mode 100644 index 000000000..5eeb6bfb7 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/FileModelHelper.java @@ -0,0 +1,411 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage; + +import java.util.List; + +import org.apache.log4j.Logger; + +import edu.cornell.mannlib.vitro.webapp.beans.DataPropertyStatementImpl; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.IndividualImpl; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatementImpl; +import edu.cornell.mannlib.vitro.webapp.beans.VClass; +import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyStatementDao; +import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.dao.InsertException; +import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyStatementDao; +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; + +/** + *

+ * A collection of methods to help manipulate the model, with regard to uploaded + * files. + *

+ *

+ * Some of the public methods are static, since the Individual that is passed as + * a parameter holds all necessary references for the operation. Other methods + * require an instance, which is initialized with a {@link WebappDaoFactory}. + *

+ */ +public class FileModelHelper { + private static final Logger log = Logger.getLogger(FileModelHelper.class); + + // ---------------------------------------------------------------------- + // Static methods -- the Individual holds all necessary references. + // ---------------------------------------------------------------------- + + /** + * Is this a FileByteStream individual? + */ + public static boolean isFileBytestream(Individual entity) { + for (VClass vClass : entity.getVClasses()) { + if (VitroVocabulary.FS_BYTESTREAM_CLASS.equals(vClass.getURI())) { + log.debug("Entity '" + entity.getURI() + "' is a bytestream"); + return true; + } + } + log.debug("Entity '" + entity.getURI() + "' is not a bytestream"); + return false; + } + + /** + * Locate the file surrogate for the main image of this entity. + * + * @return the surrogate, or null if there is no such image, or + * if the entity itself is null. + */ + public static Individual getMainImage(Individual entity) { + if (entity == null) { + return null; + } + + Individual mainFile = entity + .getRelatedIndividual(VitroVocabulary.IND_MAIN_IMAGE); + + if (mainFile == null) { + log.debug("Entity '" + entity.getURI() + + "' had no associated main image."); + return null; + } else { + log.debug("Entity '" + entity.getURI() + + "' had associated main image: '" + mainFile.getURI() + + "'"); + return mainFile; + } + } + + /** + * Locate the file surrogate for the thumbnail of this file. + * + * @return the surrogate, or null if there is no thumbnail, or + * if the file itself is null. + */ + public static Individual getThumbnailForImage(Individual fileSurrogate) { + if (fileSurrogate == null) { + return null; + } + + Individual thumbFile = fileSurrogate + .getRelatedIndividual(VitroVocabulary.FS_THUMBNAIL_IMAGE); + + if (thumbFile == null) { + log.warn("Main image file '" + fileSurrogate.getURI() + + "' had no associated thumbnail."); + return null; + } else { + log.debug("Main image file '" + fileSurrogate.getURI() + + "' had associated thumbnail: '" + thumbFile.getURI() + + "'"); + return thumbFile; + } + } + + /** + * Locate the bytestream object for this file. + * + * @return the bytestream object, or null if there is no + * bytestream, or if the file itself is null. + */ + public static Individual getBytestreamForFile(Individual fileSurrogate) { + if (fileSurrogate == null) { + return null; + } + + Individual byteStream = fileSurrogate + .getRelatedIndividual(VitroVocabulary.FS_DOWNLOAD_LOCATION); + + if (byteStream == null) { + log.error("File surrogate '" + fileSurrogate.getURI() + + "' had no associated bytestream."); + return null; + } else { + log.debug("File surroage'" + fileSurrogate.getURI() + + "' had associated bytestream: '" + byteStream.getURI() + + "'"); + return byteStream; + } + } + + /** + * Find the filename for this file. + * + * @return the filename, or null if the file itself is + * null. + */ + public static String getFilename(Individual fileSurrogate) { + if (fileSurrogate == null) { + return null; + } + + String filename = fileSurrogate + .getDataValue(VitroVocabulary.FS_FILENAME); + + if (filename == null) { + log.error("File had no filename: '" + fileSurrogate.getURI() + "'"); + } else { + log.debug("Filename for '" + fileSurrogate.getURI() + "' was '" + + filename + "'"); + } + return filename; + } + + /** + * Find the MIME type for this file. + * + * @return the MIME type, or null if the file itself is + * null. + */ + public static String getMimeType(Individual fileSurrogate) { + if (fileSurrogate == null) { + return null; + } + + String mimeType = fileSurrogate + .getDataValue(VitroVocabulary.FS_MIME_TYPE); + + if (mimeType == null) { + log.error("File had no mimeType: '" + fileSurrogate.getURI() + "'"); + } else { + log.debug("mimeType for '" + fileSurrogate.getURI() + "' was '" + + mimeType + "'"); + } + return mimeType; + } + + /** + * Return the URI for this individual, or null if the + * individual is null. + */ + private static String getUri(Individual entity) { + if (entity == null) { + return null; + } else { + return entity.getURI(); + } + } + + /** + * Locate the URI of the bytestream of the main image for this entity. + * + * @return the URI, or null if there is no such bytestream, or + * if the entity itself is null. + */ + public static String getMainImageBytestreamUri(Individual entity) { + Individual mainFile = getMainImage(entity); + Individual byteStream = getBytestreamForFile(mainFile); + return getUri(byteStream); + } + + /** + * Find the filename of the main image for this entity. + * + * @return the filename, or null if there is no such image. + */ + public static String getMainImageFilename(Individual entity) { + Individual mainFile = getMainImage(entity); + return getFilename(mainFile); + } + + /** + * Locate the individual that represents the bytestream of the thumbnail of + * the main image for this entity. + * + * @return the URI, or null if there is no such thumbnail + * image, or if the entity itself is null. + */ + public static String getThumbnailBytestreamUri(Individual entity) { + Individual mainFile = getMainImage(entity); + Individual thumbFile = getThumbnailForImage(mainFile); + Individual byteStream = getBytestreamForFile(thumbFile); + return getUri(byteStream); + } + + /** + * Find the filename of the thumbnail of the main image. + * + * @return the filename, or null if there is no such thumbnail + * image, or if the entity itself is null. + */ + public static String getThumbnailFilename(Individual entity) { + Individual mainFile = getMainImage(entity); + Individual thumbFile = getThumbnailForImage(mainFile); + return getFilename(thumbFile); + } + + // ---------------------------------------------------------------------- + // Instance methods -- need access to a WebappDaoFactory + // ---------------------------------------------------------------------- + + private final IndividualDao individualDao; + private final ObjectPropertyStatementDao objectPropertyStatementDao; + private final DataPropertyStatementDao dataPropertyStatementDao; + + public FileModelHelper(WebappDaoFactory webappDaoFactory) { + this.individualDao = webappDaoFactory.getIndividualDao(); + this.objectPropertyStatementDao = webappDaoFactory + .getObjectPropertyStatementDao(); + this.dataPropertyStatementDao = webappDaoFactory + .getDataPropertyStatementDao(); + } + + /** + * Some of these methods require an Individual as an argument. + */ + public Individual getIndividualByUri(String uri) { + return individualDao.getIndividualByURI(uri); + } + + /** + * If this URI represents a ByteStream object, we need to find it's + * surrogate object in order to find the mime type. + * + * @return the mime type, or null if we couldn't find the mime + * type, or if the bytestream object itself is null. + */ + public String getMimeTypeForBytestream(String bytestreamUri) { + if (bytestreamUri == null) { + return null; + } + + ObjectPropertyStatement opStmt = new ObjectPropertyStatementImpl(null, + VitroVocabulary.FS_DOWNLOAD_LOCATION, bytestreamUri); + List stmts = objectPropertyStatementDao + .getObjectPropertyStatements(opStmt); + if (stmts.size() > 1) { + String uris = ""; + for (ObjectPropertyStatement stmt : stmts) { + uris += "'" + stmt.getSubjectURI() + "' "; + } + log.warn("Found " + stmts.size() + " Individuals that claim '" + + bytestreamUri + "' as its bytestream:" + uris); + } + if (stmts.isEmpty()) { + log.warn("No individual claims '" + "' as its bytestream."); + return null; + } + Individual surrogate = individualDao.getIndividualByURI(stmts.get(0) + .getSubjectURI()); + + return getMimeType(surrogate); + } + + /** + * Create a file surrogate individual in the model. + */ + public Individual createFileIndividual(String mimeType, String filename, + Individual byteStream) { + Individual file = new IndividualImpl(); + file.setVClassURI(VitroVocabulary.FS_FILE_CLASS); + + String uri = null; + try { + uri = individualDao.insertNewIndividual(file); + } catch (InsertException e) { + throw new IllegalStateException( + "Failed to create the file individual.", e); + } + + dataPropertyStatementDao + .insertNewDataPropertyStatement(new DataPropertyStatementImpl( + uri, VitroVocabulary.FS_FILENAME, filename)); + dataPropertyStatementDao + .insertNewDataPropertyStatement(new DataPropertyStatementImpl( + uri, VitroVocabulary.FS_MIME_TYPE, mimeType)); + objectPropertyStatementDao + .insertNewObjectPropertyStatement(new ObjectPropertyStatementImpl( + uri, VitroVocabulary.FS_DOWNLOAD_LOCATION, byteStream + .getURI())); + + return individualDao.getIndividualByURI(uri); + } + + /** + * Create a bytestream individual in the model. + */ + public Individual createByteStreamIndividual() { + Individual byteStream = new IndividualImpl(); + byteStream.setVClassURI(VitroVocabulary.FS_BYTESTREAM_CLASS); + + String uri = null; + try { + uri = individualDao.insertNewIndividual(byteStream); + } catch (InsertException e) { + throw new IllegalStateException( + "Failed to create the bytestream individual.", e); + } + + return individualDao.getIndividualByURI(uri); + } + + /** + * Store this file surrogate as the main image on this entity. + */ + public void setAsMainImageOnEntity(Individual person, + Individual imageSurrogate) { + person.setMainImageUri(imageSurrogate.getURI()); + individualDao.updateIndividual(person); + log.debug("Set main image '" + getUri(imageSurrogate) + "' on '" + + person.getURI() + "'"); + } + + /** + * Remove the current main image from this entity. + * + * @return the file surrogate, or null if there was none. + */ + public Individual removeMainImage(Individual person) { + Individual mainImage = getMainImage(person); + person.setMainImageUri(null); + individualDao.updateIndividual(person); + log.debug("Removed main image '" + getUri(mainImage) + "' from '" + + person.getURI() + "'"); + return mainImage; + } + + /** + * Store this file surrogate as the thumnail on this entity. + */ + public void setThumbnailOnIndividual(Individual entity, + Individual thumbnailSurrogate) { + String mainImageUri = entity.getMainImageUri(); + objectPropertyStatementDao + .insertNewObjectPropertyStatement(new ObjectPropertyStatementImpl( + mainImageUri, VitroVocabulary.FS_THUMBNAIL_IMAGE, + thumbnailSurrogate.getURI())); + } + + /** + * Are there any ObjectPropertyStatements in the model whose object is this + * file surrogate? + */ + public boolean isFileReferenced(Individual surrogate) { + ObjectPropertyStatement opStmt = new ObjectPropertyStatementImpl(null, + null, surrogate.getURI()); + List stmts = objectPropertyStatementDao + .getObjectPropertyStatements(opStmt); + if (log.isDebugEnabled()) { + log.debug(stmts.size() + " statements referencing '" + + surrogate.getURI() + "'"); + for (ObjectPropertyStatement stmt : stmts) { + log.debug("'" + stmt.getSubjectURI() + "' -- '" + + stmt.getPropertyURI() + "' -- '" + + stmt.getObjectURI() + "'"); + } + } + return !stmts.isEmpty(); + } + + /** + * This file is being deleted; remove both the surrogate and its bytestream + * from the model. + */ + public void removeFileFromModel(Individual surrogate) { + Individual bytestream = getBytestreamForFile(surrogate); + individualDao.deleteIndividual(bytestream); + individualDao.deleteIndividual(surrogate); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/FileServingHelper.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/FileServingHelper.java new file mode 100644 index 000000000..dd4a3fff7 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/FileServingHelper.java @@ -0,0 +1,109 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage; + +import org.apache.log4j.Logger; + +import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; + +/** + * Static methods to help when serving uploaded files. + */ +public class FileServingHelper { + private static final Logger log = Logger.getLogger(FileServingHelper.class); + + private static final String DEFAULT_PATH = "/individual/"; + private static final String FILE_PATH = "/file/"; + private static final String DEFAULT_NAMESPACE = initializeDefaultNamespace(); + + /** + * At startup, get the default namespace from the configuration properties, + * and trim off the suffix. + */ + private static String initializeDefaultNamespace() { + String defaultNamespace = ConfigurationProperties + .getProperty(FileStorageSetup.PROPERTY_DEFAULT_NAMESPACE); + if (defaultNamespace == null) { + throw new IllegalArgumentException( + "Configuration properties must contain a value for '" + + FileStorageSetup.PROPERTY_DEFAULT_NAMESPACE + "'"); + } + + if (!defaultNamespace.endsWith(DEFAULT_PATH)) { + throw new IllegalArgumentException( + "Default namespace does not match the expected form: '" + + defaultNamespace + "'"); + } + + return defaultNamespace; + } + + /** + *

+ * Combine the URI and the filename to produce a relative URL for the file + * (relative to the context of the webapp). + *

+ *

+ * This should involve stripping the default namespace from the front of the + * URL, replacing it with the file prefix, and adding the filename to the + * end. + *

+ * + * @return
    + *
  • the translated URL, if the URI was in the default namespace,
  • + *
  • the original URI, if it wasn't in the default namespace,
  • + *
  • null, if the original URI or the filename was null.
  • + *
+ */ + public static String getBytestreamAliasUrl(String uri, String filename) { + if ((uri == null) || (filename == null)) { + return null; + } + if (!uri.startsWith(DEFAULT_NAMESPACE)) { + log.warn("uri does not start with the default namespace: '" + uri + + "'"); + return uri; + } + String remainder = uri.substring(DEFAULT_NAMESPACE.length()); + String separator = remainder.endsWith("/") ? "" : "/"; + return FILE_PATH + remainder + separator + filename; + } + + /** No need for instances because all of the methods are static. */ + private FileServingHelper() { + // nothing to instantiate. + } + + /** + *

+ * Take a relative URL (relative to the context of the webapp) and produce + * the URI for the file bytestream. + *

+ *

+ * This should involve removing the filename from the end of the URL, and + * replacing the file prefix with the default namespace. + *

+ * + * @return the URI, or null if the URL couldn't be translated. + */ + public static String getBytestreamUri(String path) { + if (path == null) { + return null; + } + if (!path.startsWith(FILE_PATH)) { + log.warn("path does not start with a file prefix: '" + path + "'"); + return null; + } + String remainder = path.substring(FILE_PATH.length()); + + int slashHere = remainder.lastIndexOf('/'); + if (slashHere == -1) { + log.debug("path does not include a filename: '" + path + "'"); + return null; + } + remainder = remainder.substring(0, slashHere); + + return DEFAULT_NAMESPACE + remainder; + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageImpl.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageImpl.java index 518d2a4ab..f41ccc215 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageImpl.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageImpl.java @@ -23,10 +23,13 @@ import java.util.Properties; import java.util.Set; import java.util.Map.Entry; +import org.apache.log4j.Logger; + /** * The default implementation of {@link FileStorage}. */ public class FileStorageImpl implements FileStorage { + private static final Logger log = Logger.getLogger(FileStorageImpl.class); private final File baseDir; private final File rootDir; @@ -290,6 +293,8 @@ public class FileStorageImpl implements FileStorage { public String getFilename(String id) throws IOException { File dir = FileStorageHelper.getPathToIdDirectory(id, this.namespacesMap, this.rootDir); + log.debug("ID '" + id + "' translates to this directory path: '" + dir + + "'"); if ((!dir.exists()) || (!dir.isDirectory())) { return null; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageSetup.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageSetup.java index e65da2c8e..87965e970 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageSetup.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/backend/FileStorageSetup.java @@ -28,13 +28,13 @@ public class FileStorageSetup implements ServletContextListener { * The default implementation will use this key to ask * {@link ConfigurationProperties} for the file storage base directory. */ - static final String PROPERTY_FILE_STORAGE_BASE_DIR = "upload.directory"; + public static final String PROPERTY_FILE_STORAGE_BASE_DIR = "upload.directory"; /** * The default implementation will use this key to ask * {@link ConfigurationProperties} for the default URI namespace. */ - static final String PROPERTY_DEFAULT_NAMESPACE = "Vitro.defaultNamespace"; + public static final String PROPERTY_DEFAULT_NAMESPACE = "Vitro.defaultNamespace"; /** * Create an implementation of {@link FileStorage} and store it in the @@ -46,7 +46,7 @@ public class FileStorageSetup implements ServletContextListener { FileStorage fs; try { File baseDirectory = figureBaseDir(); - Collection fileNamespace = figureFileNamespace(); + Collection fileNamespace = confirmDefaultNamespace(); fs = new FileStorageImpl(baseDirectory, fileNamespace); } catch (IOException e) { throw new IllegalStateException( @@ -75,16 +75,15 @@ public class FileStorageSetup implements ServletContextListener { } /** - * Get the configuration property for the default namespace, and derive the - * file namespace from it. The default namespace is assumed to be in this - * form: http://vivo.mydomain.edu/individual/ + * Get the configuration property for the default namespace, and confirm + * that it is in the proper form. The default namespace is assumed to be in + * this form: http://vivo.mydomain.edu/individual/ * * For use by the constructor in implementations of {@link FileStorage}. * - * @returns the file namespace is assumed to be in this form: - * http://vivo.mydomain.edu/file/ + * @returns a collection containing the default namespace. */ - private Collection figureFileNamespace() { + private Collection confirmDefaultNamespace() { String defaultNamespace = ConfigurationProperties .getProperty(PROPERTY_DEFAULT_NAMESPACE); if (defaultNamespace == null) { @@ -94,7 +93,6 @@ public class FileStorageSetup implements ServletContextListener { } String defaultSuffix = "/individual/"; - String fileSuffix = "/file/"; if (!defaultNamespace.endsWith(defaultSuffix)) { throw new IllegalArgumentException( @@ -102,10 +100,7 @@ public class FileStorageSetup implements ServletContextListener { + defaultNamespace + "'"); } - int hostLength = defaultNamespace.length() - defaultSuffix.length(); - String fileNamespace = defaultNamespace.substring(0, hostLength) - + fileSuffix; - return Collections.singleton(fileNamespace); + return Collections.singleton(defaultNamespace); } @Override diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/serving/FileServingServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/serving/FileServingServlet.java new file mode 100644 index 000000000..b9d2944ca --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/serving/FileServingServlet.java @@ -0,0 +1,175 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.serving; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.Logger; + +import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileServingHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; + +/** + *

+ * Handles a request to serve an uploaded file from the file storage system. + *

+ *

+ * The path of the request should be the "alias URL" of the desired file. We + * need to: + *

    + *
  • Use the alias URL to find the URI of the file bytestream object.
  • + *
  • Find the file surrogate object to get the MIME type of the file, and + * confirm the filename.
  • + *
  • Set the MIME type on the output stream and serve the bytes.
  • + *
+ *

+ *

+ * If the request is superficially correct, but no such file can be found, + * return a 404. If there is a break in the data structures within the model or + * the file system, return a 500. + *

+ */ +public class FileServingServlet extends VitroHttpServlet { + private static final Logger log = Logger + .getLogger(FileServingServlet.class); + + private FileStorage fileStorage; + + /** + * Get a reference to the File Storage system. + */ + @Override + public void init() throws ServletException { + Object o = getServletContext().getAttribute( + FileStorageSetup.ATTRIBUTE_NAME); + if (o instanceof FileStorage) { + fileStorage = (FileStorage) o; + } else { + throw new UnavailableException( + "The ServletContext did not hold a FileStorage object at '" + + FileStorageSetup.ATTRIBUTE_NAME + + "'; found this instead: " + o); + } + } + + @Override + protected void doGet(HttpServletRequest rawRequest, + HttpServletResponse response) throws ServletException, IOException { + VitroRequest request = new VitroRequest(rawRequest); + + // Use the alias URL to get the URI of the bytestream object. + String path = request.getServletPath() + request.getPathInfo(); + log.debug("Path is '" + path + "'"); + + String uri = FileServingHelper.getBytestreamUri(path); + log.debug("Bytestream URI is '" + uri + "'"); + if (uri == null) { + String message = "The request path is not valid for the File servlet: '" + + path + "'"; + log.error(message); + response.sendError(SC_INTERNAL_SERVER_ERROR, message); + return; + } + + // Validate that the file exists, with the requested URI and filename. + String requestedFilename = getFilename(path); + String actualFilename = fileStorage.getFilename(uri); + if (actualFilename == null) { + log.debug("Requested a non-existent file: " + path); + response.sendError(SC_NOT_FOUND, ("File not found: " + path)); + return; + } + if (!actualFilename.equals(requestedFilename)) { + log.warn("The requested filename does not match the " + + "actual filename; request: '" + path + "', actual: '" + + actualFilename + "'"); + response.sendError(SC_NOT_FOUND, ("File not found: " + path)); + return; + } + + // Get the MIME type. + String mimeType = new FileModelHelper(getWebappDaoFactory()) + .getMimeTypeForBytestream(uri); + + // Open the actual byte stream. + InputStream in; + try { + in = fileStorage.getInputStream(uri, actualFilename); + } catch (FileNotFoundException e) { + log.error(e); + response.sendError(SC_INTERNAL_SERVER_ERROR, e.toString()); + return; + } + + /* + * Everything is ready and working. Set the status and the content type, + * and send the image bytes. + */ + response.setStatus(SC_OK); + + if (mimeType != null) { + response.setContentType(mimeType); + } + + ServletOutputStream out = null; + try { + out = response.getOutputStream(); + byte[] buffer = new byte[8192]; + int howMany; + while (-1 != (howMany = in.read(buffer))) { + out.write(buffer, 0, howMany); + } + } finally { + try { + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } + if (out != null) { + try { + out.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + /** + * The filename is the portion of the path after the last slash. + */ + private String getFilename(String path) { + int slashHere = path.lastIndexOf('/'); + if (slashHere == -1) { + return path; + } else { + return path.substring(slashHere + 1); + } + } + + /** + * A POST request is treated the same as a GET request. + */ + @Override + protected void doPost(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + doGet(request, response); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/AllThumbsAdjuster.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/AllThumbsAdjuster.java new file mode 100644 index 000000000..68ee1101a --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/AllThumbsAdjuster.java @@ -0,0 +1,71 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; +import java.io.IOException; + +import com.hp.hpl.jena.rdf.model.ResIterator; +import com.hp.hpl.jena.rdf.model.Resource; + +/** + * Adjust any individual that has a thumbnail with no main image. + */ +public class AllThumbsAdjuster extends FsuScanner { + protected final File imageDirectory; + + public AllThumbsAdjuster(FSUController controller) { + super(controller); + this.imageDirectory = controller.getImageDirectory(); + } + + /** + * For every individual with thumbnails but no main images, create a main + * image from the first thumbnail. + */ + public void adjust() { + updateLog.section("Creating main images for thumbnails " + + "that have none."); + + ResIterator haveThumb = model.listResourcesWithProperty(thumbProperty); + try { + while (haveThumb.hasNext()) { + Resource resource = haveThumb.next(); + + if (resource.getProperty(imageProperty) == null) { + createMainImageFromThumbnail(resource); + } + } + } finally { + haveThumb.close(); + } + } + + /** + * This individual has a thumbnail but no main image. Create one. + *
    + *
  • Figure a name for the main image.
  • + *
  • Copy the thumbnail image file into the main image file.
  • + *
  • Set that file as an image (old-style) on the individual.
  • + *
+ */ + private void createMainImageFromThumbnail(Resource resource) { + String thumbFilename = getValues(resource, thumbProperty).get(0); + String mainFilename = addFilenamePrefix("_main_image_", thumbFilename); + updateLog.log(resource, "creating a main file at '" + mainFilename + + "' to match the thumbnail at '" + thumbFilename + "'"); + + try { + File thumbFile = new File(imageDirectory, thumbFilename); + File mainFile = new File(imageDirectory, mainFilename); + mainFile = checkNameConflicts(mainFile); + FileUtil.copyFile(thumbFile, mainFile); + + resource.addProperty(imageProperty, mainFilename); + } catch (IOException e) { + updateLog.error(resource, "failed to create main file '" + + mainFilename + "'", e); + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/DeadEndPropertyRemover.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/DeadEndPropertyRemover.java new file mode 100644 index 000000000..ef135c64c --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/DeadEndPropertyRemover.java @@ -0,0 +1,73 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.ResIterator; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.Statement; + +/** + * Removes any image properties (main or thumbnail) that point to files that + * don't actually exist. + */ +public class DeadEndPropertyRemover extends FsuScanner { + protected final File imageDirectory; + + public DeadEndPropertyRemover(FSUController controller) { + super(controller); + this.imageDirectory = controller.getImageDirectory(); + } + + /** + * Remove dead end properties for both main images and thumbnails. + */ + public void remove() { + updateLog.section("Removing image properties whose " + + "referenced files do not exist."); + + removeDeadEndProperties(imageProperty, "main image"); + removeDeadEndProperties(thumbProperty, "thumbnail"); + } + + /** + * Check all of the individuals that possess this property. + */ + private void removeDeadEndProperties(Property prop, String label) { + ResIterator resources = model.listResourcesWithProperty(prop); + try { + while (resources.hasNext()) { + Resource resource = resources.next(); + removeDeadEndPropertiesFromResource(resource, prop, label); + } + } finally { + resources.close(); + } + } + + /** + * Check these statments on this resource. If any of them does not point to + * an existing file, remove the statement. + */ + private void removeDeadEndPropertiesFromResource(Resource resource, + Property prop, String label) { + for (Statement stmt : getStatements(resource, prop)) { + RDFNode node = stmt.getObject(); + if (node.isLiteral()) { + String filename = ((Literal)node).getString(); + File file = new File(imageDirectory, filename); + if (!file.exists()) { + updateLog.warn(resource, "removing link to " + label + " '" + + filename + "': file does not exist at '" + + file.getAbsolutePath() + "'."); + model.remove(stmt); + } + } + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FSUController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FSUController.java new file mode 100644 index 000000000..0f7156731 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FSUController.java @@ -0,0 +1,38 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; + +import com.hp.hpl.jena.rdf.model.Model; + +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; + +/** + * An object that can initialize one of the {@link FsuScanner}s. + */ +public interface FSUController { + + /** The Jena model. */ + Model getModel(); + + /** The update log. */ + FSULog getUpdateLog(); + + /** The directory where the old-style images were stored. */ + File getImageDirectory(); + + /** The file storage system. */ + FileStorage getFileStorage(); + + /** A file model helper with access to the DAO layer. */ + FileModelHelper getFileModelHelper(); + + /** Where to store the files that were translated. */ + File getTranslatedDirectory(); + + /** Where to store the files that weren't in use anyway. */ + File getUnreferencedDirectory(); + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FSULog.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FSULog.java new file mode 100644 index 000000000..7406ae1e9 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FSULog.java @@ -0,0 +1,180 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.Statement; + +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; + +/** + * Writes the log file for the {@link FileStorageUpdater}. Be sure to call + * {@link #close()} when finished. + */ +public class FSULog { + private final SimpleDateFormat timeStamper = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + private final File logFile; + private final PrintWriter writer; + private boolean open; + + FSULog(File logDirectory) throws IOException { + this.logFile = generateTimestampedFilename(logDirectory); + this.writer = new PrintWriter(this.logFile); + open = true; + } + + /** + * Create a filename for the log file that contains a timestamp, so if we + * run the process more than once, we will see multiple files. + */ + private File generateTimestampedFilename(File upgradeDirectory) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-sss"); + String filename = "upgradeLog." + sdf.format(new Date()) + ".txt"; + return new File(upgradeDirectory, filename); + } + + /** + * Where are we writing the output? + */ + String getFilename() { + return this.logFile.getAbsolutePath(); + } + + /** + * Write this message. + */ + void log(String message) { + writer.println(timeStamper.format(new Date()) + " INFO " + message); + } + + /** + * Write this message about this resource. + */ + void log(Resource resource, String message) { + log(showResource(resource) + message); + } + + /** + * Write this warning message. + */ + public void warn(String message) { + writer.println(timeStamper.format(new Date()) + " WARN " + message); + } + + /** + * Write this warning message about this resource. + */ + public void warn(Resource resource, String message) { + warn(showResource(resource) + message); + } + + /** + * Write this error message. + */ + void error(String message) { + writer.println(timeStamper.format(new Date()) + " ERROR " + message); + } + + /** + * Write this exception as an error message.. + */ + void error(Exception e) { + error(e.toString()); + e.printStackTrace(writer); + } + + /** + * Write an error message with this exception. + */ + public void error(String message, Exception e) { + error(message); + e.printStackTrace(writer); + } + + /** + * Write an error message about this resource and with this exception. + */ + public void error(Resource resource, String message) { + error(showResource(resource) + message); + } + + /** + * Write an error message about this resource and with this exception. + */ + public void error(Resource resource, String message, Exception e) { + error(showResource(resource) + message, e); + } + + /** + * Write a section heading. + */ + public void section(String message) { + log(">>>>>>>>>> "); + log(">>>>>>>>>> " + message); + log(">>>>>>>>>> "); + } + + /** + * Close the writer, if not already closed. + */ + public void close() { + if (open) { + writer.close(); + open = false; + } + } + + /** + * Format the resource label and URI for output in a message. + */ + private String showResource(Resource resource) { + return "On resource '" + getLabel(resource) + "' (" + getUri(resource) + + "), "; + } + + /** + * Find the URI for this resource, if there is one. + */ + private String getUri(Resource resource) { + if (resource != null) { + String uri = resource.getURI(); + if (uri != null) { + return uri; + } + } + return "no URI"; + } + + /** + * Find the label for this resource, if there is one. + */ + private String getLabel(Resource resource) { + if (resource != null) { + Model model = resource.getModel(); + if (model != null) { + Property prop = model.createProperty(VitroVocabulary.LABEL); + Statement stmt = resource.getProperty(prop); + if (stmt != null) { + RDFNode node = stmt.getObject(); + if (node.isLiteral()) { + return ((Literal) node).getString(); + } + } + } + } + return "no label"; + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FileStorageUpdater.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FileStorageUpdater.java new file mode 100644 index 000000000..6017637f6 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FileStorageUpdater.java @@ -0,0 +1,272 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.ResIterator; + +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; + +/** + *

+ * Clean up any files that are stored in the old directory structure and + * referenced by old-style image properties. + *

+ *

+ * Besides converting the files to the new framework, this process will produce + * these artifacts: + *

    + *
  • A log file in the uploaded files directory, with a timestamped name, such + * as upgrade/upgradeLog2010-06-20T14-55-00.txt, for example. If + * for any reason, the upgrade process must run again, the log will not be + * overwritten.
  • + *
  • A directory of "deleted" files - these were extra thumbnail files or + * extra main image files -- see the details below.
  • + *
  • A directory of "unreferenced" files - these were in the image directory, + * but not connected to any entity.
  • + *
+ *

+ *

+ * We consider some special cases: + *

    + *
  • An individual may refer to an image file that does not actually exist. If + * so, that reference will be deleted.
  • + *
  • There may be more than one reference to the same image file. If so, all + * but the first such reference will be deleted.
  • + *
  • + * In the old style, it was possible to have a main image without a thumbnail. + * If we find that, we will generate a scaled down copy of the main image, store + * that as a thumbnail image file, and then proceed.
  • + *
  • + * In the old style, it was possible to have a thumbnail without a main image. + * If we find that, we will make a copy of the thumbnail image file, declare + * that copy to be the main image, and then proceed.
  • + *
  • + * We may find individuals with more than one main image, or more than one + * thumbnail. If so, we will discard all but the first one (move them to the + * "deleted" directory).
  • + *
+ *

+ *

+ * Aside from these special cases, we will: + *

    + *
  • Translate the main image. + *
      + *
    • Store the image in the new file system.
    • + *
    • Delete the image from the old images directory.
    • + *
    • Create a ByteStream individual for the main image.
    • + *
    • Create a Surrogate individual for the main image.
    • + *
    • Tie these together and attach to the entity that owns the image.
    • + *
    + *
  • + *
  • Translate the thumbnail. + *
      + *
    • Store the thumbnail in the new file system.
    • + *
    • Create a ByteStream individual for the thumbnail.
    • + *
    • Create a Surrogate individual for the thumbnail.
    • + *
    • Tie these together and attach to the Surrogate for the main image.
    • + *
    + *
  • + *
+ *

+ *

+ * After processing all of these cases, there may be some images remaining in + * the "images" directory. These will be moved to the "unreferenced" directory, + * while preserving any internal tree structure. + *

+ */ +public class FileStorageUpdater implements FSUController { + private static final Log log = LogFactory.getLog(FileStorageUpdater.class); + + /** How wide should a generated thumbnail image be (in pixels)? */ + public static final int THUMBNAIL_WIDTH = 150; + + /** How high should a generated thumbnail image be (in pixels)? */ + public static final int THUMBNAIL_HEIGHT = 150; + + /** How is the main image referenced in the old scheme? */ + public static final String IMAGEFILE = VitroVocabulary.vitroURI + + "imageFile"; + + /** How is the thumbnail referenced in the old scheme? */ + public static final String IMAGETHUMB = VitroVocabulary.vitroURI + + "imageThumb"; + + private final Model model; + + private final FileStorage fileStorage; + private final FileModelHelper fileModelHelper; + private final File imageDirectory; + private final File upgradeDirectory; + + private FSULog updateLog; + + public FileStorageUpdater(WebappDaoFactory wadf, Model model, + FileStorage fileStorage, File uploadDirectory) { + this.model = model; + this.fileStorage = fileStorage; + this.fileModelHelper = new FileModelHelper(wadf); + this.imageDirectory = new File(uploadDirectory, "images"); + this.upgradeDirectory = new File(uploadDirectory, "upgrade"); + } + + /** + *

+ * Go through all of the individuals who have image files or thumbnail + * files, adjusting them to the new way. + *

+ *

+ * If there is nothing to do, don't even create a log file, just exit. + *

+ *

+ * If there is something to do, go through the whole process. + *

+ *

+ * At the end, there should be nothing to do. If that's true, clean out the + * old images directory. + *

+ */ + public void update() { + // If there is nothing to do, we're done: don't even create a log file. + if (!isThereAnythingToDo()) { + log.debug("Found no pre-1.1 file references."); + return; + } + + // Create the upgrade directory and the log file. + setup(); + + try { + // Remove any image properties that don't point to literals. + new NonLiteralPropertyRemover(this).remove(); + + // Remove any image properties that point to files that don't exist. + new DeadEndPropertyRemover(this).remove(); + + // No resource may have multiple main images or multiple thumbnails. + new MultiplePropertyRemover(this).remove(); + + // Create a main image for any thumbnail that doesn't have one. + new AllThumbsAdjuster(this).adjust(); + + // Create a thumbnail for any main image that doesn't have one. + new NoThumbsAdjuster(this).adjust(); + + // Copy all images into the new file storage system, translating + // into the new schema. Get a list of all the images we translated. + ImageSchemaTranslater translater = new ImageSchemaTranslater(this); + Collection translatedFiles = translater.translate(); + + if (isThereAnythingToDo()) { + throw new IllegalStateException( + "FileStorageUpdate was unsuccessful -- " + + "model still contains pre-1.1 file references."); + } + + // Clean out the old image directory, separating into files which + // were translated, and files for which we found no reference. + new ImageDirectoryCleaner(this).clean(translatedFiles); + } finally { + updateLog.close(); + } + + log.info("Finished updating pre-1.1 file references."); + } + + /** + * Query the model. If there are any resources with old-style image + * properties, we have work to do. + */ + private boolean isThereAnythingToDo() { + ResIterator haveImage = model.listResourcesWithProperty(model + .createProperty(IMAGEFILE)); + try { + if (haveImage.hasNext()) { + return true; + } + } finally { + haveImage.close(); + } + + ResIterator haveThumb = model.listResourcesWithProperty(model + .createProperty(IMAGETHUMB)); + try { + if (haveThumb.hasNext()) { + return true; + } + } finally { + haveThumb.close(); + } + + return false; + } + + /** + * Create the upgrade directory. Create the log file. If we fail, drop dead. + */ + private void setup() { + try { + this.upgradeDirectory.mkdirs(); + updateLog = new FSULog(this.upgradeDirectory); + log.info("Updating pre-1.1 file references. Log file is " + + updateLog.getFilename()); + } catch (IOException e) { + if (updateLog != null) { + updateLog.close(); + } + throw new IllegalStateException("can't create log file: '" + + updateLog.getFilename() + "'", e); + } + } + + // ---------------------------------------------------------------------- + // Methods to set up the individual scanners. + // ---------------------------------------------------------------------- + + @Override + public Model getModel() { + return this.model; + } + + @Override + public FSULog getUpdateLog() { + return this.updateLog; + } + + @Override + public FileModelHelper getFileModelHelper() { + return this.fileModelHelper; + } + + @Override + public FileStorage getFileStorage() { + return this.fileStorage; + } + + @Override + public File getImageDirectory() { + return this.imageDirectory; + + } + + @Override + public File getTranslatedDirectory() { + return new File(this.upgradeDirectory, "translatedImages"); + } + + @Override + public File getUnreferencedDirectory() { + return new File(this.upgradeDirectory, "unreferencedImages"); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FileUtil.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FileUtil.java new file mode 100644 index 000000000..95b40eba1 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FileUtil.java @@ -0,0 +1,107 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A collection of static routines for moving, copying and deleting files. + */ +public class FileUtil { + /** + * Copy a file from one location to another, and remove it from the original + * location. + */ + public static void moveFile(File from, File to) throws IOException { + copyFile(from, to); + deleteFile(from); + } + + /** + * Copy a file from one location to another. + */ + public static void copyFile(File from, File to) throws IOException { + if (!from.exists()) { + throw new FileNotFoundException("File '" + from.getAbsolutePath() + + "' does not exist."); + } + + InputStream in = null; + try { + in = new FileInputStream(from); + writeFile(in, to); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Create a file with the contents of this data stream. + * + * @param stream + * the data stream. You must close it afterward. + */ + public static void writeFile(InputStream stream, File to) + throws IOException { + if (to.exists()) { + throw new IOException("File '" + to.getAbsolutePath() + + "' already exists."); + } + + File parent = to.getParentFile(); + if (!parent.exists()) { + parent.mkdirs(); + if (!parent.exists()) { + throw new IOException("Can't create parent directory for '" + + to.getAbsolutePath() + "'"); + } + } + + OutputStream out = null; + try { + out = new FileOutputStream(to); + byte[] buffer = new byte[8192]; + int howMany; + while (-1 != (howMany = stream.read(buffer))) { + out.write(buffer, 0, howMany); + } + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Delete this file, and make sure that it's gone. + */ + public static void deleteFile(File file) throws IOException { + file.delete(); + if (file.exists()) { + throw new IOException("Failed to delete file '" + + file.getAbsolutePath() + "'"); + } + } + + /** No need to instantiate it -- all methods are static. */ + private FileUtil() { + // Nothing to instantiate. + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FsuScanner.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FsuScanner.java new file mode 100644 index 000000000..6a1e6b59e --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/FsuScanner.java @@ -0,0 +1,122 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.Statement; +import com.hp.hpl.jena.rdf.model.StmtIterator; + +/** + * Base class for the tools that scan the model. Holds some useful fields and + * some utility methods. + */ +public abstract class FsuScanner { + protected final Model model; + protected final FSULog updateLog; + + protected final Property imageProperty; + protected final Property thumbProperty; + + public FsuScanner(FSUController controller) { + this.model = controller.getModel(); + this.updateLog = controller.getUpdateLog(); + + this.imageProperty = model.createProperty(FileStorageUpdater.IMAGEFILE); + this.thumbProperty = model + .createProperty(FileStorageUpdater.IMAGETHUMB); + } + + /** + * Read all of the specified properties on a resource, and return a + * {@link List} of the {@link String} values. + */ + protected List getValues(Resource resource, Property property) { + List list = new ArrayList(); + StmtIterator stmts = resource.listProperties(property); + try { + while (stmts.hasNext()) { + Statement stmt = stmts.next(); + RDFNode object = stmt.getObject(); + if (object.isLiteral()) { + list.add(((Literal) object).getString()); + } else { + updateLog.error(resource, + "property value was not a literal: " + + "property is '" + property.getURI() + + "', value is '" + object + "'"); + } + } + } finally { + stmts.close(); + } + return list; + } + + /** + * Read all of the specified properties on a resource, and return a + * {@link List} of the {@link Statement}s. + */ + protected List getStatements(Resource resource, Property property) { + List list = new ArrayList(); + StmtIterator stmts = resource.listProperties(property); + try { + while (stmts.hasNext()) { + list.add(stmts.next()); + } + } finally { + stmts.close(); + } + return list; + } + + /** + * Find the filename within a path so we can add this prefix to it, while + * retaining the path. + */ + protected String addFilenamePrefix(String prefix, String path) { + int slashHere = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + if (slashHere == -1) { + return prefix + path; + } else { + String dirs = path.substring(0, slashHere + 1); + String filename = path.substring(slashHere + 1); + return dirs + prefix + filename; + } + } + + /** + * We are about to create a file - if a file of this name already exists, + * increment the name until we have no collision. + * + * @return the original file, or the file with the incremented name. + */ + protected File checkNameConflicts(final File file) { + if (!file.exists()) { + // No conflict. + return file; + } + + File parent = file.getParentFile(); + String filename = file.getName(); + for (int i = 0; i < 100; i++) { + File newFile = new File(parent, i + filename); + if (!newFile.exists()) { + updateLog.log("File '" + file + "' already exists, using '" + + newFile + "' to avoid conflict."); + return newFile; + } + } + + updateLog.error("File '" + file + + "' already exists. Unable to avoid conflict."); + return file; + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/ImageDirectoryCleaner.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/ImageDirectoryCleaner.java new file mode 100644 index 000000000..93ec8c9ec --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/ImageDirectoryCleaner.java @@ -0,0 +1,121 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; + +/** + * Clean out the old image directory. Copy the files into the upgrade directory, + * separating into the ones that we translated, and the ones that weren't + * referenced. + */ +public class ImageDirectoryCleaner extends FsuScanner { + protected final File imageDirectory; + protected final File translatedDirectory; + protected final File unreferencedDirectory; + + public ImageDirectoryCleaner(FSUController controller) { + super(controller); + this.imageDirectory = controller.getImageDirectory(); + this.translatedDirectory = controller.getTranslatedDirectory(); + this.unreferencedDirectory = controller.getUnreferencedDirectory(); + } + + /** + * Remove all of the files from the old image directory. + */ + public void clean(Collection translatedFiles) { + updateLog.section("Cleaning the old image directory of " + + "files that were translated."); + removeTranslatedFiles(translatedFiles); + + updateLog.section("Cleaning the old image directory of " + + "files that were not referenced."); + removeRemainingFiles(imageDirectory); + } + + /** + * Move all of the files that we translated into the new system. + */ + private void removeTranslatedFiles(Collection translatedFiles) { + for (String path : translatedFiles) { + updateLog.log("moving image file '" + path + + "' to the 'translated' directory."); + File oldFile = new File(imageDirectory, path); + File deletedFile = new File(translatedDirectory, path); + try { + FileUtil.moveFile(oldFile, deletedFile); + } catch (IOException e) { + updateLog.error("Failed to move translated file '" + + oldFile.getAbsolutePath() + "'"); + } + } + } + + /** + * Go through the images directory, and discard any that remain. They must + * not have been referenced by any existing individuals. + */ + private void removeRemainingFiles(File directory) { + updateLog.log("Cleaning image directory '" + directory + "'"); + try { + File targetDirectory = makeCorrespondingDirectory(directory); + File[] children = directory.listFiles(); + if (children != null) { + for (File child : children) { + if (child.isDirectory()) { + removeRemainingFiles(child); + } else { + moveUnreferencedFile(targetDirectory, child); + } + } + } + } catch (IOException e) { + updateLog.error("Failed to clean images directory '" + + directory.getAbsolutePath() + "'", e); + } + } + + /** + * Move this file from its current location to its new home in the + * "unreferenced" directory. Log it. + */ + private void moveUnreferencedFile(File targetDirectory, File file) { + updateLog.log("Moving image file '" + file.getPath() + + "' to the 'unreferenced' directory"); + try { + File newFile = new File(targetDirectory, file.getName()); + FileUtil.moveFile(file, newFile); + } catch (IOException e) { + updateLog.error("Can't move unreferenced file '" + + file.getAbsolutePath() + "'", e); + } + } + + /** + * Figure out the path from the "images" directory to this one, and create a + * corresponding directory in the "unreferenced" area. + */ + private File makeCorrespondingDirectory(File directory) throws IOException { + String imagesPath = imageDirectory.getAbsolutePath(); + String thisPath = directory.getAbsolutePath(); + + if (!thisPath.startsWith(imagesPath)) { + throw new IOException("Can't make a corresponding directory for '" + + thisPath + "'"); + } + + String suffix = thisPath.substring(imagesPath.length()); + + File corresponding = new File(unreferencedDirectory, suffix); + corresponding.mkdirs(); + if (!corresponding.exists()) { + throw new IOException("Failed to create corresponding directory '" + + corresponding.getAbsolutePath() + "'"); + } + + return corresponding; + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/ImageSchemaTranslater.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/ImageSchemaTranslater.java new file mode 100644 index 000000000..5734d2ab2 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/ImageSchemaTranslater.java @@ -0,0 +1,191 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.io.FilenameUtils; + +import com.hp.hpl.jena.rdf.model.ResIterator; +import com.hp.hpl.jena.rdf.model.Resource; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.filestorage.FileModelHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; + +/** + * Make copies of the main image and thumbnail in the new file storage system, + * and in the model. Remove the old properties, but don't remove the old files + * yet, in case someone else is referring to them also. + */ +public class ImageSchemaTranslater extends FsuScanner { + protected final File imageDirectory; + protected final FileModelHelper fileModelHelper; + protected final FileStorage fileStorage; + + public ImageSchemaTranslater(FSUController controller) { + super(controller); + this.imageDirectory = controller.getImageDirectory(); + this.fileStorage = controller.getFileStorage(); + this.fileModelHelper = controller.getFileModelHelper(); + } + + /** + * By the time we get here, any individual with a main image also has a + * thumbnail, and vice versa, and exactly one of each. For each one, + * translate the main image and the thumbnail into the new system. + */ + public Collection translate() { + updateLog.section("Copying images into the new file storage, " + + "and adding them to the new model."); + + SortedSet translated = new TreeSet(); + ResIterator haveImage = model.listResourcesWithProperty(imageProperty); + try { + while (haveImage.hasNext()) { + Resource resource = haveImage.next(); + translateImages(resource, translated); + } + } finally { + haveImage.close(); + } + return translated; + } + + /** + * This individual should have exactly one main image and exactly one + * thumbnail. + *
    + *
  • Translate the first main image into the new system.
  • + *
  • Translate the first thumbnail into the new system.
  • + *
  • Remove all old-style main image properties.
  • + *
  • Remove all old-style thumbnail properties.
  • + *
+ */ + private void translateImages(Resource resource, + Collection translated) { + List mainImages = getValues(resource, imageProperty); + if (mainImages.size() != 1) { + updateLog.error(resource, "has " + mainImages.size() + + " main images: " + mainImages); + return; + } + + translateMainImage(resource, mainImages.get(0)); + translated.add(mainImages.get(0)); + resource.removeAll(imageProperty); + + List thumbnails = getValues(resource, thumbProperty); + if (thumbnails.size() != 1) { + updateLog.error(resource, "has " + thumbnails.size() + + " thumbnails: " + thumbnails); + return; + } + + translateThumbnail(resource, thumbnails.get(0)); + translated.add(thumbnails.get(0)); + resource.removeAll(thumbProperty); + } + + /** + * Translate the main image into the new system + */ + private void translateMainImage(Resource resource, String path) { + Individual file = translateFile(resource, path, "main image"); + Individual person = fileModelHelper.getIndividualByUri(resource + .getURI()); + fileModelHelper.setAsMainImageOnEntity(person, file); + } + + /** + * Translate the thumbnail into the new system. + */ + private void translateThumbnail(Resource resource, String path) { + Individual file = translateFile(resource, path, "thumbnail"); + Individual person = fileModelHelper.getIndividualByUri(resource + .getURI()); + fileModelHelper.setThumbnailOnIndividual(person, file); + } + + /** + * Translate an image file into the new system + *
    + *
  • Create a new File, FileByteStream.
  • + *
  • Attempt to infer MIME type.
  • + *
  • Copy into the File system.
  • + *
+ * + * @return the new File surrogate. + */ + private Individual translateFile(Resource resource, String path, + String label) { + File oldFile = new File(imageDirectory, path); + String filename = getSimpleFilename(path); + String mimeType = guessMimeType(resource, filename); + + // Create the file individuals in the model + Individual byteStream = fileModelHelper.createByteStreamIndividual(); + Individual file = fileModelHelper.createFileIndividual(mimeType, + filename, byteStream); + + updateLog.log(resource, "translating " + label + " '" + path + + "' into the file storage as '" + file.getURI() + "'"); + + InputStream inputStream = null; + try { + // Store the file in the FileStorage system. + inputStream = new FileInputStream(oldFile); + fileStorage.createFile(byteStream.getURI(), filename, inputStream); + } catch (IOException e) { + updateLog.error(resource, "Can't create the " + label + " file. ", + e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return file; + } + + /** + * Remove any path parts, and just get the filename and extension. + */ + private String getSimpleFilename(String path) { + return FilenameUtils.getName(path); + } + + /** + * Guess what the MIME type might be. + */ + private String guessMimeType(Resource resource, String filename) { + if (filename.endsWith(".gif") || filename.endsWith(".GIF")) { + return "image/gif"; + } else if (filename.endsWith(".png") || filename.endsWith(".PNG")) { + return "image/png"; + } else if (filename.endsWith(".jpg") || filename.endsWith(".JPG")) { + return "image/jpeg"; + } else if (filename.endsWith(".jpeg") || filename.endsWith(".JPEG")) { + return "image/jpeg"; + } else if (filename.endsWith(".jpe") || filename.endsWith(".JPE")) { + return "image/jpeg"; + } else { + updateLog.warn(resource, + "can't recognize the MIME type of this image file: '" + + filename + "'"); + return "image"; + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/MultiplePropertyRemover.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/MultiplePropertyRemover.java new file mode 100644 index 000000000..e2161f7da --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/MultiplePropertyRemover.java @@ -0,0 +1,71 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.util.List; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.ResIterator; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.Statement; + +/** + * If a resource has more than one image or more than one thumbnail, this + * discards the extras. + */ +public class MultiplePropertyRemover extends FsuScanner { + + public MultiplePropertyRemover(FSUController controller) { + super(controller); + } + + /** + * By now, we have removed any non-literals or dead ends, so keep the first + * one and discard any extras. + */ + public void remove() { + updateLog.section("Checking for resources with more " + + "than one main image, or more than one thumbnail."); + + removeExtraProperties(imageProperty, "main image"); + removeExtraProperties(thumbProperty, "thumbnail"); + } + + /** + * Check each resource that has this property. + */ + public void removeExtraProperties(Property prop, String label) { + ResIterator resources = model.listResourcesWithProperty(prop); + try { + while (resources.hasNext()) { + Resource resource = resources.next(); + removeExtraProperties(resource, prop, label); + } + } finally { + resources.close(); + } + } + + /** + * If this resource has more than one of this property, delete the extras. + */ + private void removeExtraProperties(Resource resource, Property prop, + String label) { + List stmts = getStatements(resource, prop); + for (int i = 1; i < stmts.size(); i++) { + Statement stmt = stmts.get(i); + RDFNode node = stmt.getObject(); + if (node.isLiteral()) { + String value = ((Literal) node).getString(); + updateLog.warn(resource, "removing extra " + label + + " property: '" + value + "'"); + } else { + updateLog.warn(resource, "removing extra " + label + + " property: '" + node + "'"); + } + model.remove(stmt); + } + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/NoThumbsAdjuster.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/NoThumbsAdjuster.java new file mode 100644 index 000000000..02182b184 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/NoThumbsAdjuster.java @@ -0,0 +1,103 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import com.hp.hpl.jena.rdf.model.ResIterator; +import com.hp.hpl.jena.rdf.model.Resource; + +/** + * Adjust any individual that has a main image but no thumbnail. + */ +public class NoThumbsAdjuster extends FsuScanner { + protected final File imageDirectory; + + public NoThumbsAdjuster(FSUController controller) { + super(controller); + this.imageDirectory = controller.getImageDirectory(); + } + + /** + * For every individual with main images but no thumbnails, create a + * thumbnail from the first main image. + */ + public void adjust() { + updateLog.section("Creating thumbnails to match main images."); + + ResIterator haveImage = model.listResourcesWithProperty(imageProperty); + try { + while (haveImage.hasNext()) { + Resource resource = haveImage.next(); + + if (resource.getProperty(thumbProperty) == null) { + createThumbnailFromMainImage(resource); + } + } + } finally { + haveImage.close(); + } + } + + /** + * This individual has a main image but no thumbnail. Create one. + *
    + *
  • Figure a name for the thumbnail image.
  • + *
  • Make a scaled copy of the main image into the thumbnail.
  • + *
  • Set that file as a thumbnail (old-style) on the individual.
  • + *
+ */ + private void createThumbnailFromMainImage(Resource resource) { + String mainFilename = getValues(resource, imageProperty).get(0); + String thumbFilename = addFilenamePrefix("_thumbnail_", mainFilename); + updateLog.log(resource, "creating a thumbnail at '" + thumbFilename + + "' from the main image at '" + mainFilename + "'"); + + File mainFile = new File(imageDirectory, mainFilename); + File thumbFile = new File(imageDirectory, thumbFilename); + thumbFile = checkNameConflicts(thumbFile); + try { + generateThumbnailImage(mainFile, thumbFile, + FileStorageUpdater.THUMBNAIL_WIDTH, + FileStorageUpdater.THUMBNAIL_HEIGHT); + + resource.addProperty(thumbProperty, thumbFilename); + } catch (IOException e) { + updateLog.error(resource, "failed to create thumbnail file '" + + thumbFilename + "'", e); + } + } + + /** + * Read in the main image, and scale it to a thumbnail that maintains the + * aspect ratio, but doesn't exceed either of these dimensions. + */ + private void generateThumbnailImage(File mainFile, File thumbFile, + int maxWidth, int maxHeight) throws IOException { + BufferedImage bsrc = ImageIO.read(mainFile); + + double scale = Math.min(((double) maxWidth) / bsrc.getWidth(), + ((double) maxHeight) / bsrc.getHeight()); + AffineTransform at = AffineTransform.getScaleInstance(scale, scale); + int newWidth = (int) (scale * bsrc.getWidth()); + int newHeight = (int) (scale * bsrc.getHeight()); + updateLog.log("Scaling '" + mainFile + "' by a factor of " + scale + + ", from " + bsrc.getWidth() + "x" + bsrc.getHeight() + " to " + + newWidth + "x" + newHeight); + + BufferedImage bdest = new BufferedImage(newWidth, newHeight, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = bdest.createGraphics(); + + g.drawRenderedImage(bsrc, at); + + ImageIO.write(bdest, "JPG", thumbFile); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/NonLiteralPropertyRemover.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/NonLiteralPropertyRemover.java new file mode 100644 index 000000000..2c9222fef --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/filestorage/updater/NonLiteralPropertyRemover.java @@ -0,0 +1,78 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage.updater; + +import java.util.ArrayList; +import java.util.List; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.ResIterator; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.Statement; +import com.hp.hpl.jena.rdf.model.StmtIterator; + +/** + * All image properties should have literal values. Burn any that don't. + */ +public class NonLiteralPropertyRemover extends FsuScanner { + + public NonLiteralPropertyRemover(FSUController controller) { + super(controller); + } + + /** + * Remove any image properties whose objects are not {@link Literal}s. + */ + public void remove() { + updateLog.section("Checking for image properties whose objects " + + "are not literals."); + + removeNonLiterals(imageProperty, "image file"); + removeNonLiterals(thumbProperty, "thumnail"); + } + + /** + * Check all resources for bogus values on this property. + */ + private void removeNonLiterals(Property prop, String label) { + ResIterator resources = model.listResourcesWithProperty(prop); + try { + while (resources.hasNext()) { + Resource resource = resources.next(); + removeNonLiterals(resource, prop, label); + } + } finally { + resources.close(); + } + } + + /** + * Check this resource for bogus values onthis property. + */ + private void removeNonLiterals(Resource resource, Property prop, + String label) { + List bogusValues = new ArrayList(); + StmtIterator stmts = resource.listProperties(prop); + try { + while (stmts.hasNext()) { + Statement stmt = stmts.next(); + RDFNode object = stmt.getObject(); + if (!object.isLiteral()) { + bogusValues.add(object); + } + } + } finally { + stmts.close(); + } + + for (RDFNode bogusValue : bogusValues) { + updateLog.warn(resource, "discarding " + label + + " property with non-literal as object: '" + bogusValue + + "'"); + model.createStatement(resource, prop, bogusValue).remove(); + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/UpdateUploadedFiles.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/UpdateUploadedFiles.java new file mode 100644 index 000000000..348c9382b --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/servlet/setup/UpdateUploadedFiles.java @@ -0,0 +1,85 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.servlet.setup; + +import java.io.File; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.apache.log4j.Logger; + +import com.hp.hpl.jena.ontology.OntModel; + +import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.dao.jena.JenaBaseDao; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorage; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; +import edu.cornell.mannlib.vitro.webapp.filestorage.updater.FileStorageUpdater; + +/** + * TODO + */ +public class UpdateUploadedFiles implements ServletContextListener { + private static final Logger log = Logger + .getLogger(UpdateUploadedFiles.class); + + /** + * Nothing to do on teardown. + */ + @Override + public void contextDestroyed(ServletContextEvent sce) { + return; + } + + /** + * Check that the ontology model, the old upload directory, and the file + * storage system are all valid. Then do the update. + */ + @Override + public void contextInitialized(ServletContextEvent sce) { + try { + ServletContext ctx = sce.getServletContext(); + + WebappDaoFactory wadf = (WebappDaoFactory) ctx + .getAttribute("webappDaoFactory"); + if (wadf == null) { + throw new IllegalStateException("Webapp DAO Factory is null"); + } + + OntModel jenaOntModel = (OntModel) ctx + .getAttribute(JenaBaseDao.JENA_ONT_MODEL_ATTRIBUTE_NAME); + if (jenaOntModel == null) { + throw new IllegalStateException("Ontology model is null"); + } + + FileStorage fileStorage = (FileStorage) ctx + .getAttribute(FileStorageSetup.ATTRIBUTE_NAME); + if (fileStorage == null) { + throw new IllegalStateException("File storage system is null"); + } + + String uploadDirectoryName = ConfigurationProperties + .getProperty(FileStorageSetup.PROPERTY_FILE_STORAGE_BASE_DIR); + if (uploadDirectoryName == null) { + throw new IllegalStateException("Upload directory name is null"); + } + File uploadDirectory = new File(uploadDirectoryName); + if (!uploadDirectory.exists()) { + throw new IllegalStateException("Upload directory '" + + uploadDirectory.getAbsolutePath() + + "' does not exist."); + } + + + FileStorageUpdater fsu = new FileStorageUpdater(wadf, jenaOntModel, + fileStorage, uploadDirectory); + fsu.update(); + } catch (Exception e) { + log.error("Unknown problem", e); + throw new RuntimeException(e); + } + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/FrontEndEditingUtils.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/FrontEndEditingUtils.java index 97029e551..f8e01c112 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/FrontEndEditingUtils.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/FrontEndEditingUtils.java @@ -21,7 +21,6 @@ public class FrontEndEditingUtils { private static final List VITRO_NS_DATA_PROPS = Arrays.asList(VitroVocabulary.BLURB, VitroVocabulary.CITATION, VitroVocabulary.DESCRIPTION, - VitroVocabulary.IMAGETHUMB, VitroVocabulary.LABEL, VitroVocabulary.MONIKER // VitroVocabulary.RDF_TYPE, diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/view/IndividualView.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/view/IndividualView.java index 0dd1706e1..7955534f6 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/view/IndividualView.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/view/IndividualView.java @@ -108,11 +108,11 @@ public class IndividualView extends ViewObject { return individual.getKeywords(); } - public String getImageFile() { - return individual.getImageFile(); + public String getImageUrl() { + return individual.getImageUrl(); } - public String getImageThumb() { - return individual.getImageThumb(); + public String getThumbUrl() { + return individual.getThumbUrl(); } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/jsptags/PropertyEditLinks.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/jsptags/PropertyEditLinks.java index 7dbfd1604..a1c6e13c8 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/jsptags/PropertyEditLinks.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/jsptags/PropertyEditLinks.java @@ -235,7 +235,7 @@ public class PropertyEditLinks extends TagSupport{ if( contains( allowedAccessTypeArray, EditLinkAccess.ADDNEW ) ){ log.debug("vitro namespace property "+propertyUri+" gets an \"add\" link"); LinkStruct ls = null; - if (propertyUri.equals(VitroVocabulary.IMAGETHUMB)) { + if (propertyUri.equals(VitroVocabulary.IND_MAIN_IMAGE)) { ls = getImageLink(subjectUri, contextPath, "add"); } else { String url = makeRelativeHref(contextPath +"edit/editDatapropStmtRequestDispatch.jsp", @@ -346,7 +346,7 @@ public class PropertyEditLinks extends TagSupport{ LinkStruct[] links = new LinkStruct[2]; - if (predicateUri.equals(VitroVocabulary.IMAGETHUMB)) { + if (predicateUri.equals(VitroVocabulary.IND_MAIN_IMAGE)) { if( contains( allowedAccessTypeArray, EditLinkAccess.MODIFY ) ){ log.debug("permission found to UPDATE vitro namepsace property statement "+ predicateUri); links[0] = getImageLink(subjectUri, contextPath, "edit"); @@ -643,7 +643,7 @@ public class PropertyEditLinks extends TagSupport{ "entityUri", subjectUri); ls.setHref(url); ls.setType(action); - ls.setMouseoverText("upload a new image"); + ls.setMouseoverText("upload a new image"); return ls; } diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoTest.java index 562098684..0cb3a7168 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoTest.java @@ -2,6 +2,8 @@ package edu.cornell.mannlib.vitro.webapp.dao.jena; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import java.io.StringReader; import junit.framework.Assert; @@ -15,7 +17,6 @@ import com.hp.hpl.jena.ontology.OntProperty; import com.hp.hpl.jena.ontology.Restriction; import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.ModelFactory; -import com.hp.hpl.jena.rdf.model.StmtIterator; import com.hp.hpl.jena.vocabulary.OWL; import com.hp.hpl.jena.vocabulary.RDF; import com.hp.hpl.jena.vocabulary.RDFS; @@ -110,12 +111,8 @@ public class JenaBaseDaoTest { isDependentRelation ; Model expectedModel = (ModelFactory.createOntologyModel()).read(new StringReader(expected), "", "N3"); - - //modtime times make it difficult to compare graphs - wipeOutModTime(expectedModel); - wipeOutModTime(ontModel); - - Assert.assertTrue( ontModel.isIsomorphicWith(expectedModel) ); + + assertEquivalentModels(expectedModel, ontModel); } catch (InsertException e) { Assert.fail(e.getMessage()); } @@ -192,11 +189,7 @@ public class JenaBaseDaoTest { Model expectedModel = (ModelFactory.createOntologyModel()).read(new StringReader(expected), "", "N3"); - //modtime times make it difficult to compare graphs - wipeOutModTime(expectedModel); - wipeOutModTime(model); - - Assert.assertTrue( model.isIsomorphicWith(expectedModel)); + assertEquivalentModels(expectedModel, model); } @@ -256,11 +249,7 @@ public class JenaBaseDaoTest { Model expectedModel = (ModelFactory.createOntologyModel()).read(new StringReader(expected), "", "N3"); - //modtime times make it difficult to compare graphs - wipeOutModTime(expectedModel); - wipeOutModTime(ontModel); - - Assert.assertTrue( ontModel.isIsomorphicWith(expectedModel) ); + assertEquivalentModels(expectedModel, ontModel); } catch (InsertException e) { Assert.fail(e.getMessage()); } @@ -367,17 +356,7 @@ public class JenaBaseDaoTest { // wipeOutModTime(model); // Assert.assertTrue( model.isIsomorphicWith(expectedModel)); // } - void printModels(Model expected, Model result){ - System.out.println("Expected:"); - expected.write(System.out); - System.out.println("Result:"); - result.write(System.out); - } - void wipeOutModTime(Model model){ - model.removeAll(null, model.createProperty(VitroVocabulary.MODTIME), null); - } - @Test /** * Tests that any statements with a property as predicate are removed @@ -478,5 +457,32 @@ public class JenaBaseDaoTest { Assert.assertEquals(m.size(), 2); // just rdf:type for Class1 and Prop } + + /** + * Compare the contents of the expected model with the actual model (not counting modification times). + */ + private void assertEquivalentModels(Model expected, Model actual) { + // modtime times make it difficult to compare graphs + wipeOutModTime(expected); + wipeOutModTime(actual); + + if (actual.isIsomorphicWith(expected)) { + return; + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream p = new PrintStream(out, true); + p.println("Models do not match: expected <"); + expected.write(out); + p.println("> but was <"); + actual.write(out); + p.println(">"); + Assert.fail(out.toString()); + } + + private void wipeOutModTime(Model model){ + model.removeAll(null, model.createProperty(VitroVocabulary.MODTIME), null); + } + } diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/filestorage/FileServingHelperTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/filestorage/FileServingHelperTest.java new file mode 100644 index 000000000..27ac87fee --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/filestorage/FileServingHelperTest.java @@ -0,0 +1,120 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.filestorage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import javax.naming.InitialContext; + +import org.apache.log4j.Level; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import stubs.javax.naming.InitialContextStub; +import stubs.javax.naming.spi.InitialContextFactoryStub; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.filestorage.backend.FileStorageSetup; + +/** + */ +public class FileServingHelperTest extends AbstractTestClass { + private static final String DEFAULT_NAMESPACE = "http://some.crazy.domain/individual/"; + private static final String CONFIG_PROPERTIES = "#mock config properties file\n"; + private static File tempDir; + + // ---------------------------------------------------------------------- + // framework + // ---------------------------------------------------------------------- + + /** + * Use a mock {@link InitialContext} to create an empty + * {@link ConfigurationProperties} object. Each test can use + * {@link #setConfigurationProperties(String, String)} to populate it as + * they choose. + */ + @BeforeClass + public static void createConfigurationProperties() throws Exception { + tempDir = createTempDirectory("FileServingHelperTest"); + + File propsFile = createFile(tempDir, "config.properties", + CONFIG_PROPERTIES); + + System.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY, + InitialContextFactoryStub.class.getName()); + InitialContextStub.reset(); + new InitialContext().bind("java:comp/env/path.configuration", propsFile + .getPath()); + + setConfigurationProperties(DEFAULT_NAMESPACE); + } + + @AfterClass + public static void cleanup() { + purgeDirectoryRecursively(tempDir); + } + + // ---------------------------------------------------------------------- + // tests + // ---------------------------------------------------------------------- + + @Test + public void nullUri() { + assertCorrectUrl(null, "somefilename.ext", null); + } + + @Test + public void nullFilename() { + assertCorrectUrl("http://some.crazy.domain/individual/n4324", null, + null); + } + + @Test + public void notInDefaultNamespace() { + setLoggerLevel(FileServingHelper.class, Level.ERROR); + assertCorrectUrl("notInTheNamespace", + "somefilename.ext", "notInTheNamespace"); + } + + @Test + public void inDefaultNamespaceNoTrailingSlash() { + assertCorrectUrl("http://some.crazy.domain/individual/n4324", + "somefilename.ext", "/file/n4324/somefilename.ext"); + } + + @Test + public void inDefaultNamespaceTrailingSlash() { + assertCorrectUrl("http://some.crazy.domain/individual/n4324/", + "somefilename.ext", "/file/n4324/somefilename.ext"); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private static void setConfigurationProperties(String defaultNamespace) { + Map map = new HashMap(); + map.put(FileStorageSetup.PROPERTY_DEFAULT_NAMESPACE, defaultNamespace); + + try { + Field f = ConfigurationProperties.class.getDeclaredField("theMap"); + f.setAccessible(true); + f.set(null, map); + } catch (Exception e) { + fail("Exception while setting config properties: " + e); + } + } + + private void assertCorrectUrl(String uri, String filename, String expected) { + String actual = FileServingHelper.getBytestreamAliasUrl(uri, filename); + assertEquals("url", expected, actual); + } + +} diff --git a/webapp/web/templates/edit/specific/entity_retry.jsp b/webapp/web/templates/edit/specific/entity_retry.jsp index 55ca3aa45..bf13772a1 100644 --- a/webapp/web/templates/edit/specific/entity_retry.jsp +++ b/webapp/web/templates/edit/specific/entity_retry.jsp @@ -103,17 +103,5 @@ - - - Thumbnail Filename Optional and usually more convenient to upload from previous screen
- " maxlength="255" /> -

- - - Optional Larger Image (filename or full path) - " maxlength="255" /> -

- - diff --git a/webapp/web/templates/edit/specific/entity_retry_init.jsp b/webapp/web/templates/edit/specific/entity_retry_init.jsp index 7aa420930..2c9b45906 100644 --- a/webapp/web/templates/edit/specific/entity_retry_init.jsp +++ b/webapp/web/templates/edit/specific/entity_retry_init.jsp @@ -90,18 +90,6 @@ " size="19" maxlength="19">
- - - - Thumbnail Filename Optional and usually more convenient to upload from previous screen - " size="60" maxlength="255" /> - - - - Optional Larger Image (filename or full path) - " size="60" maxlength="255" /> - - diff --git a/webapp/web/templates/entity/entityBasic.jsp b/webapp/web/templates/entity/entityBasic.jsp index 6bbd477e5..862961093 100644 --- a/webapp/web/templates/entity/entityBasic.jsp +++ b/webapp/web/templates/entity/entityBasic.jsp @@ -63,18 +63,8 @@ if (VitroRequestPrep.isSelfEditing(request) || LoginFormBean.loggedIn(request, L - <% - //here we build up the url for the larger image. - String imageUrl = null; - if (entity.getImageFile() != null && - entity.getImageFile().indexOf("http:")==0) { - imageUrl = entity.getImageFile(); - } else { - imageUrl = response.encodeURL( "/images/" + entity.getImageFile() ); - } - //anytime we are at an entity page we shouldn't have an editing config or submission session.removeAttribute("editjson"); EditConfiguration.clearAllConfigsInSession(session); @@ -196,28 +186,26 @@ if (VitroRequestPrep.isSelfEditing(request) || LoginFormBean.loggedIn(request, L <%-- Thumbnail (with citation) --%> - +
- - + +

image

- +
- +
@@ -282,7 +270,7 @@ if (VitroRequestPrep.isSelfEditing(request) || LoginFormBean.loggedIn(request, L <%-- Citation, if no thumbnail --%> - + diff --git a/webapp/web/templates/entity/entityListForGalleryTab.jsp b/webapp/web/templates/entity/entityListForGalleryTab.jsp index fa7348772..50a2af4d3 100644 --- a/webapp/web/templates/entity/entityListForGalleryTab.jsp +++ b/webapp/web/templates/entity/entityListForGalleryTab.jsp @@ -34,7 +34,6 @@ - @@ -43,15 +42,14 @@ - + diff --git a/webapp/web/templates/entity/entityListForTabs.jsp b/webapp/web/templates/entity/entityListForTabs.jsp index b7961779f..9d6362905 100644 --- a/webapp/web/templates/entity/entityListForTabs.jsp +++ b/webapp/web/templates/entity/entityListForTabs.jsp @@ -25,7 +25,6 @@ <%/* just moving this into page scope for easy use */ %> -
    @@ -76,13 +75,12 @@ | ">${entLink.anchor} - + - - +
    ${ent.blurb}
    diff --git a/webapp/web/templates/search/searchGroup.jsp b/webapp/web/templates/search/searchGroup.jsp index 36c0af7a9..59e669848 100644 --- a/webapp/web/templates/search/searchGroup.jsp +++ b/webapp/web/templates/search/searchGroup.jsp @@ -1,55 +1,54 @@ <%-- $This file is distributed under the terms of the license in /doc/license.txt$ --%> -<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %><%/* this odd thing points to something in web.xml */ %> -<%@ page errorPage="/error.jsp"%> -<% /*********************************************** - Display a single search result group - - request.attributes: - a List with objects with the named "entities" - a ClassGroup object named "classgroup" - - request.parameters: - None yet. - - Consider sticking < % = MiscWebUtils.getReqInfo(request) % > in the html output - for debugging info. - **********************************************/ - if (request.getAttribute("entities") == null){ - String e="searchGroup.jsp expects that request attribute 'entities' be set to the Entity object to display."; - throw new JspException(e); - } - if (request.getAttribute("classgroup") == null){ - String e="searchGroup.jsp expects that request attribute 'classgroup' be set to the Entity object to display."; - throw new JspException(e); - } -%> - -<%/* just moving this into page scope for easy use */ %> - - - - - -
    -

    - - - ${entity.anchor} - - - | - - - - - <%/* here we import the properties for the entity */ %> -
    - -
    - -
    +<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %><%/* this odd thing points to something in web.xml */ %> +<%@ page errorPage="/error.jsp"%> +<% /*********************************************** + Display a single search result group + + request.attributes: + a List with objects with the named "entities" + a ClassGroup object named "classgroup" + + request.parameters: + None yet. + + Consider sticking < % = MiscWebUtils.getReqInfo(request) % > in the html output + for debugging info. + **********************************************/ + if (request.getAttribute("entities") == null){ + String e="searchGroup.jsp expects that request attribute 'entities' be set to the Entity object to display."; + throw new JspException(e); + } + if (request.getAttribute("classgroup") == null){ + String e="searchGroup.jsp expects that request attribute 'classgroup' be set to the Entity object to display."; + throw new JspException(e); + } +%> +<%/* just moving this into page scope for easy use */ %> + + + + + +
    +

    + + + ${entity.anchor} + + + | + + + + + <%/* here we import the properties for the entity */ %> +
    + +
    + +
    diff --git a/webapp/web/themes/default/jsp/dashboard.jsp b/webapp/web/themes/default/jsp/dashboard.jsp index afd9a99ad..246327602 100644 --- a/webapp/web/themes/default/jsp/dashboard.jsp +++ b/webapp/web/themes/default/jsp/dashboard.jsp @@ -19,16 +19,13 @@ -
    class="loggedIn"> - - - - + + + - - " title="click to view larger image in new window" alt="" width="150"/> - + +
    ${entity.citation}
    diff --git a/webapp/web/themes/enhanced/jsp/dashboard.jsp b/webapp/web/themes/enhanced/jsp/dashboard.jsp index afd9a99ad..246327602 100644 --- a/webapp/web/themes/enhanced/jsp/dashboard.jsp +++ b/webapp/web/themes/enhanced/jsp/dashboard.jsp @@ -19,16 +19,13 @@ -
" > - - " title="${ent.name}" alt="${ent.name}" /> + ${ent.name}