001: package org.tigris.scarab.om;
002:
003: /* ================================================================
004: * Copyright (c) 2000-2005 CollabNet. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are
008: * met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in the
015: * documentation and/or other materials provided with the distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowlegement: "This product includes
019: * software developed by Collab.Net <http://www.Collab.Net/>."
020: * Alternately, this acknowlegement may appear in the software itself, if
021: * and wherever such third-party acknowlegements normally appear.
022: *
023: * 4. The hosted project names must not be used to endorse or promote
024: * products derived from this software without prior written
025: * permission. For written permission, please contact info@collab.net.
026: *
027: * 5. Products derived from this software may not use the "Tigris" or
028: * "Scarab" names nor may "Tigris" or "Scarab" appear in their names without
029: * prior written permission of Collab.Net.
030: *
031: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
032: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
033: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
034: * IN NO EVENT SHALL COLLAB.NET OR ITS CONTRIBUTORS BE LIABLE FOR ANY
035: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
036: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
037: * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
038: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
039: * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
040: * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
041: * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042: *
043: * ====================================================================
044: *
045: * This software consists of voluntary contributions made by many
046: * individuals on behalf of Collab.Net.
047: */
048:
049: import java.io.File;
050: import java.io.BufferedInputStream;
051: import java.io.BufferedOutputStream;
052: import java.io.FileInputStream;
053: import java.io.FileNotFoundException;
054: import java.io.FileOutputStream;
055: import java.io.IOException;
056:
057: import java.sql.Connection;
058: import java.util.Date;
059: import java.util.List;
060: import org.apache.torque.Torque;
061:
062: import org.apache.torque.TorqueException;
063: import org.apache.torque.om.Persistent;
064:
065: import org.apache.turbine.Turbine;
066: import org.apache.torque.util.Criteria;
067:
068: import org.apache.commons.configuration.Configuration;
069: import org.apache.commons.fileupload.FileItem;
070:
071: import org.tigris.scarab.tools.localization.L10NKeySet;
072: import org.tigris.scarab.util.Log;
073: import org.tigris.scarab.util.ScarabConstants;
074: import org.tigris.scarab.util.ScarabException;
075: import org.tigris.scarab.util.word.SearchIndex;
076: import org.tigris.scarab.util.word.SearchFactory;
077:
078: /**
079: * Attachments contain data associated with an issue. It used to be that
080: * an issue could have multiple attachments of a given type but only one
081: * value for a given Attribute. Attributes are now multi-valued, so the
082: * difference is blurred in some cases. A comment given as a reason for a
083: * modification to attribute values is considered an Attachment.
084: * Notes and urls are also considered attachments, though these two could
085: * probably be implemented as attributes (with some ui redesign).
086: * The obvious form of attachment is a file uploaded and associated with
087: * an issue, such as a screenshot showing an error or a patch.
088: *
089: * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
090: * @author <a href="mailto:jon@collab.net">Jon S. Stevens</a>
091: * @version $Id: Attachment.java 9977 2005-12-09 00:40:59Z hair $
092: */
093: public class Attachment extends BaseAttachment implements Persistent {
094: /** ObjectKey for a file type attachment */
095: public static final Integer FILE__PK = new Integer(1);
096: /** ObjectKey for a note/comment type attachment */
097: public static final Integer COMMENT__PK = new Integer(2);
098: /** ObjectKey for a url type attachment */
099: public static final Integer URL__PK = new Integer(3);
100: /** ObjectKey for a reason for modification type attachment */
101: public static final Integer MODIFICATION__PK = new Integer(4);
102:
103: /** Path to the base location for storing files */
104: private static String fileRepo = null;
105:
106: private long size = -1;
107:
108: /**
109: * The FileItem that is associated with the attachment as it is uploaded
110: * from an html form.
111: */
112: private FileItem fileItem;
113: private static Configuration configuration;
114:
115: /**
116: * Makes sure to only save the simple filename which is the part
117: * following the last path separator. This is appended as the last
118: * part of the the path returned by getRelativePath() following the
119: * It would generally be set to original filename as given on the
120: * client that uploaded the file. Spaces are replaced by underscores.
121: */
122: public void setFileName(String name) {
123: if (name == null) {
124: super .setFileName(null);
125: } else {
126: // look for both '/' and '\' as path separators
127: int start = name.lastIndexOf('/') + 1;
128: if (start == 0) {
129: start = name.lastIndexOf('\\') + 1;
130: }
131: // don't allow spaces
132: String tmpName = name.substring(start).replace(' ', '_');
133: tmpName = tmpName.replace('%', '_');
134: super .setFileName(tmpName);
135: }
136: }
137:
138: /**
139: * There is no reason to reconstruct the FileItem, always returns null.
140: * This is not used, but required by the bean introspector used by intake.
141: * @return value of file.
142: */
143: public FileItem getFile() {
144: return fileItem;
145: }
146:
147: /**
148: * Set the value of file.
149: * @param v Value to assign to file.
150: */
151: public void setFile(FileItem v) {
152: fileItem = v;
153: if (getMimeType() == null) {
154: setMimeType(v.getContentType());
155: }
156: setFileName(v.getName());
157: }
158:
159: /**
160: * Populates fields for a text (non-file) type of attachment.
161: */
162: public void setTextFields(final ScarabUser user, final Issue issue,
163: final Integer typeId) throws TorqueException {
164: setIssue(issue);
165: setTypeId(typeId);
166: setMimeType("text/plain");
167: //setCreatedDate(new Date());
168: setCreatedBy(user.getUserId());
169: }
170:
171: /**
172: * This is a little method that uses getData() to make a http url
173: * if it isn't already prefixed with "htt://"
174: */
175: public String doMakeURLFromData() {
176: String url = getData();
177: if (AttachmentTypePeer.URL_PK.equals(getTypeId())) {
178: int stop = Math.min(url.indexOf('/'), url.indexOf('?'));
179: String test = null;
180: if (stop > 0) {
181: test = url.substring(0, stop);
182: } else {
183: test = url;
184: }
185: int colon = test.indexOf(':');
186: if (colon < 0) {
187: // add default http protocol
188: StringBuffer sb = new StringBuffer(url.length() + 7);
189: sb.append("http://").append(url);
190: url = sb.toString();
191: }
192: }
193: return url;
194: }
195:
196: /**
197: * Calls super.save(Connection) and also checks for a FileItem. if one
198: * exists the file is moved to its final location.
199: *
200: * @param dbCon a <code>DBConnection</code> value
201: * @exception TorqueException if an error occurs
202: */
203: public void save(Connection dbCon) throws TorqueException {
204: if (getIssue().isNew()) {
205: throw new TorqueException(
206: "Cannot save an attachment before saving"
207: + " the issue to which it is attached."); //EXCEPTION
208: }
209: // It would be better (from an oo perspective) to do this whenever
210: // setData is called, but we can't be sure the typeId will be
211: // set prior to setting the url, so we will do the check here.
212: setData(doMakeURLFromData());
213:
214: // need to handle the case where we don't want to be smart
215: // and just set the dates to be whatever we want them
216: // to be (xml import!).
217: if (isNew()
218: && (getCreatedDate() == null && getModifiedDate() == null)) {
219: Date now = new Date();
220: setCreatedDate(now);
221: setModifiedDate(now);
222: } else if (isModified()) {
223: setModifiedDate(new Date());
224: }
225:
226: super .save(dbCon);
227:
228: try {
229: FileItem file = getFile();
230: if (file != null) {
231: File uploadFile = new File(getRepositoryDirectory(),
232: getRelativePath());
233: File parent = uploadFile.getParentFile();
234: if (!parent.exists()) {
235: mkdirs(parent);
236: }
237: file.write(uploadFile);
238: }
239: } catch (Exception e) {
240: throw new TorqueException(e); //EXCEPTION
241: }
242:
243: /*
244: * index the text for searching.
245: */
246: if (AttachmentTypePeer.COMMENT_PK.equals(getTypeId())) {
247: try {
248: SearchIndex searchIndex = SearchFactory.getInstance();
249: if (searchIndex != null) {
250: searchIndex.index(this );
251: }
252: } catch (Exception e) {
253: throw new TorqueException(e); //EXCEPTION
254: }
255: }
256: }
257:
258: /**
259: * Delete the attachment file on disk
260: * @return true if the file was deleted, false otherwise
261: */
262: public boolean deletePhysicalAttachment() throws TorqueException,
263: ScarabException {
264: File f = new File(getFullPath());
265: return f.delete();
266: }
267:
268: /**
269: * creates the directory given by path, if it does not already exist
270: */
271: private static synchronized void mkdirs(File path) {
272: if (!path.exists()) {
273: path.mkdirs();
274: }
275: }
276:
277: /**
278: * The path to an attachment file relative to the base file repository.
279: * Files are saved according to:
280: * <code>moduleId/(issue_IdCount/1000)/issueID_attID_filename</code>
281: * where moduleId and attId are primary keys of the related module and
282: * this attachment. issueID is the unique id generally used to specify
283: * the issue within the ui. issue_IdCount is the numerical suffix of
284: * the unique id. So if the pk of module PACS is 201 and this attachment
285: * pk is 123 the path would be: 201/0/PACS5_123_diff.txt or if the issue
286: * count were higher: 201/2/PACS2115_123_diff.txt. The first two
287: * directories are used to keep the number of files per directory
288: * reasonable while the issue unique id and the final textual filename
289: * allow someone browsing the file system to be better able to pick
290: * out relevant files.
291: */
292: public String getRelativePath() throws TorqueException,
293: ScarabException {
294: if (isNew()) {
295: throw new ScarabException(L10NKeySet.ExceptionPathNotSet);
296: }
297: String path = null;
298: String filename = getFileName();
299: if (filename != null) {
300: // moduleId/(issue_IdCount/1000)/issueID_attID_filename
301: StringBuffer sb = new StringBuffer(30 + filename.length());
302: final Issue issue = getIssue();
303: sb.append("mod").append(issue.getModule().getQueryKey())
304: .append(File.separator).append(
305: issue.getIdCount() / 1000).append(
306: File.separator).append(issue.getUniqueId())
307: .append('_').append(getQueryKey()).append('_')
308: .append(filename);
309: path = sb.toString();
310: }
311: return path;
312: }
313:
314: /**
315: * @return Prepends the base repository path to the path returned
316: * by {@link #getRelativePath()}, returns <code>null</code> {@link
317: * #getRelativePath()} does.
318: */
319: public String getFullPath() throws TorqueException, ScarabException {
320: String path = null;
321: final String relativePath = getRelativePath();
322: if (relativePath != null) {
323: path = getRepositoryDirectory() + File.separator
324: + relativePath;
325: }
326: return path;
327: }
328:
329: /**
330: * Get the repository path info as given in the configuration. if the
331: * path begins with a '/', it is assumed to be absolute. Otherwise
332: * the path is constructed relative to the webapp directory.
333: */
334: public static String getRepositoryDirectory() {
335: if (fileRepo == null) {
336: String testPath = getConfiguration().getString(
337: ScarabConstants.ATTACHMENTS_REPO_KEY);
338:
339: File testDir = new File(testPath);
340: if (testDir.isAbsolute()) {
341: if (!testDir.exists()) {
342: mkdirs(testDir);
343: }
344: fileRepo = testPath;
345: } else {
346: // test for existence within the webapp directory.
347: testPath = Turbine.getRealPath(testPath);
348: testDir = new File(testPath);
349: if (!testDir.exists()) {
350: mkdirs(testDir);
351: }
352: fileRepo = testPath;
353: }
354: }
355: return fileRepo;
356: }
357:
358: private static Configuration getConfiguration() {
359: if (configuration == null) {
360: configuration = Turbine.getConfiguration();
361: }
362: return configuration;
363: }
364:
365: protected static void setConfiguration(Configuration configuration) {
366: Attachment.configuration = configuration;
367: }
368:
369: public void copyFileTo(final String path) throws TorqueException,
370: ScarabException, FileNotFoundException, IOException {
371: copyFileFromTo(getFullPath(), path);
372: }
373:
374: /**
375: * Get the attachment file size. It reads this information from the
376: * FileSystem (this information is not saved into the database)
377: * @return the number of bytes or -1 if there's some kind of problem
378: * @throws TorqueException
379: */
380: public long getSize() throws TorqueException, ScarabException {
381: if (size == -1) {
382: Log
383: .get(this .getClass().getName())
384: .debug(
385: "getSize() reading attachment size from FileSystem");
386: final String path = getFullPath();
387: if (path != null) {
388: final File f = new File(getFullPath());
389: if (f != null && f.exists()) {
390: size = f.length();
391: }
392: }
393: } else {
394: Log.get(this .getClass().getName()).debug(
395: "getSize() reading attachment size from cache");
396: }
397: return size;
398: }
399:
400: public void copyFileFromTo(final String from, final String path)
401: throws TorqueException, FileNotFoundException, IOException {
402: BufferedInputStream in = null;
403: BufferedOutputStream out = null;
404: try {
405: File f = new File(path);
406: if (!f.getParentFile().exists()) {
407: f.getParentFile().mkdirs();
408: }
409:
410: in = new BufferedInputStream(new FileInputStream(from));
411: out = new BufferedOutputStream(new FileOutputStream(f));
412: final byte[] bytes = new byte[2048];
413: int nbrRead = 0;
414: while ((nbrRead = in.read(bytes)) != -1) {
415: out.write(bytes, 0, nbrRead);
416: }
417: } finally {
418: try {
419: in.close();
420: } catch (IOException e) {
421: Log.get().debug(e.getMessage());
422: }
423: try {
424: out.close();
425: } catch (IOException e) {
426: Log.get().debug(e.getMessage());
427: }
428: }
429: }
430:
431: /**
432: * Makes a copy of this object.
433: * It creates a new object filling in the simple attributes.
434: */
435: public Attachment copy() throws TorqueException {
436: Attachment copyObj = AttachmentManager.getInstance();
437: copyObj.setIssueId(getIssueId());
438: copyObj.setTypeId(getTypeId());
439: copyObj.setName(getName());
440: copyObj.setData(getData());
441: copyObj.setFileName(getFileName());
442: copyObj.setMimeType(getMimeType());
443: copyObj.setModifiedBy(getModifiedBy());
444: copyObj.setCreatedBy(getCreatedBy());
445: copyObj.setModifiedDate(getModifiedDate());
446: copyObj.setCreatedDate(getCreatedDate());
447: copyObj.setDeleted(getDeleted());
448: return copyObj;
449: }
450:
451: /**
452: * Retrieves the Activity in which this attachment was created.
453: */
454: public Activity getActivity() throws TorqueException {
455: Activity activity = null;
456: Criteria crit = new Criteria().add(ActivityPeer.ATTACHMENT_ID,
457: getAttachmentId());
458:
459: List activities = ActivityPeer.doSelect(crit);
460: if (activities.size() > 0) {
461: activity = (Activity) activities.get(0);
462: }
463: return activity;
464: }
465: }
|