001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jlan;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsResource;
032import org.opencms.file.types.CmsResourceTypeXmlContent;
033import org.opencms.file.wrapper.CmsObjectWrapper;
034import org.opencms.file.wrapper.CmsWrappedResource;
035import org.opencms.lock.CmsLock;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.util.CmsUUID;
039
040import java.io.IOException;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.List;
044import java.util.regex.Pattern;
045
046import org.apache.commons.logging.Log;
047
048import org.alfresco.jlan.server.filesys.AccessDeniedException;
049import org.alfresco.jlan.server.filesys.FileAttribute;
050import org.alfresco.jlan.server.filesys.FileInfo;
051import org.alfresco.jlan.server.filesys.NetworkFile;
052import org.alfresco.jlan.smb.SeekType;
053import org.alfresco.jlan.util.WildCard;
054
055/**
056 * This class represents a file for use by the JLAN server component. It currently just
057 * wraps an OpenCms resource.<p>
058 */
059public class CmsJlanNetworkFile extends NetworkFile {
060
061    /** The logger instance for this class. */
062    private static final Log LOG = CmsLog.getLog(CmsJlanNetworkFile.class);
063
064    /** The buffer used for reading/writing file contents. */
065    private CmsFileBuffer m_buffer = new CmsFileBuffer();
066
067    /** Flag which indicates whether the buffer has been initialized. */
068    private boolean m_bufferInitialized;
069
070    /** The CMS context to use. */
071    private CmsObjectWrapper m_cms;
072
073    /** The write count after which the file was last flushed. */
074    private int m_lastFlush;
075
076    /** The wrapped resource. */
077    private CmsResource m_resource;
078
079    /** Flag which indicates whether we need to unlock the resource. */
080    private boolean m_needToUnlock;
081
082    /** Creates a new network file instance.<p>
083     *
084     * @param cms the CMS object wrapper to use
085     * @param resource the actual CMS resource
086     * @param fullName the raw repository path
087     */
088    public CmsJlanNetworkFile(CmsObjectWrapper cms, CmsResource resource, String fullName) {
089
090        super(resource.getName());
091        m_resource = resource;
092        m_cms = cms;
093        updateFromResource();
094        setFullName(normalizeName(fullName));
095        setFileId(resource.getStructureId().hashCode());
096    }
097
098    /**
099     * @see org.alfresco.jlan.server.filesys.NetworkFile#closeFile()
100     */
101    @Override
102    public void closeFile() throws IOException {
103
104        if (hasDeleteOnClose()) {
105            delete();
106        } else {
107            flushFile();
108            if ((getWriteCount() > 0) && m_needToUnlock) {
109                try {
110                    m_cms.unlockResource(m_cms.getSitePath(m_resource));
111                    m_needToUnlock = false;
112                } catch (CmsException e) {
113                    LOG.error("Couldn't unlock file: " + m_resource.getRootPath());
114                }
115            }
116        }
117    }
118
119    /**
120     * Deletes the file.<p>
121     *
122     * @throws IOException if something goes wrong
123     */
124    public void delete() throws IOException {
125
126        try {
127            load(false);
128            ensureLock();
129            m_cms.deleteResource(m_cms.getSitePath(m_resource), CmsResource.DELETE_PRESERVE_SIBLINGS);
130            if (!m_resource.getState().isNew()) {
131                try {
132                    m_cms.unlockResource(m_cms.getSitePath(m_resource));
133                } catch (CmsException e) {
134                    LOG.warn(e.getLocalizedMessage(), e);
135                }
136            }
137        } catch (CmsException e) {
138            throw CmsJlanDiskInterface.convertCmsException(e);
139        }
140    }
141
142    /**
143     * @see org.alfresco.jlan.server.filesys.NetworkFile#flushFile()
144     */
145    @Override
146    public void flushFile() throws IOException {
147
148        int writeCount = getWriteCount();
149        Boolean ignoreErrors = (Boolean)m_cms.getRequestContext().getAttribute(
150            CmsJlanRepository.JLAN_IGNORE_WRITE_ERRORS);
151        if (ignoreErrors == null) {
152            ignoreErrors = Boolean.FALSE;
153        }
154        try {
155            if (writeCount > m_lastFlush) {
156                CmsFile file = getFile();
157                if (file != null) {
158                    CmsWrappedResource wr = new CmsWrappedResource(file);
159                    String rootPath = m_cms.getRequestContext().addSiteRoot(
160                        CmsJlanDiskInterface.getCmsPath(getFullName()));
161                    wr.setRootPath(rootPath);
162                    file = wr.getFile();
163                    byte[] content = m_buffer.getContents();
164                    if (CmsResourceTypeXmlContent.isXmlContent(file)) {
165                        content = removeTrailingNulBytes(content);
166                    }
167                    file.setContents(content);
168                    ensureLock();
169                    m_cms.writeFile(file);
170                }
171            }
172            m_lastFlush = writeCount;
173        } catch (Exception e) {
174            LOG.error(e.getLocalizedMessage(), e);
175            if (!ignoreErrors.booleanValue()) {
176                throw new IOException(e);
177            }
178        }
179
180    }
181
182    /**
183     * Gets the file information record.<p>
184     *
185     * @return the file information for this file
186     *
187     * @throws IOException if reading the file information fails
188     */
189    public FileInfo getFileInfo() throws IOException {
190
191        try {
192            load(false);
193            if (m_resource.isFile()) {
194
195                //  Fill in a file information object for this file/directory
196
197                long flen = m_resource.getLength();
198
199                //long alloc = (flen + 512L) & 0xFFFFFFFFFFFFFE00L;
200                long alloc = flen;
201                int fattr = 0;
202                if (m_cms.getRequestContext().getCurrentProject().isOnlineProject()) {
203                    fattr += FileAttribute.ReadOnly;
204                }
205                //  Create the file information
206                FileInfo finfo = new FileInfo(m_resource.getName(), flen, fattr);
207                long fdate = m_resource.getDateLastModified();
208                finfo.setModifyDateTime(fdate);
209                finfo.setAllocationSize(alloc);
210                finfo.setFileId(m_resource.getStructureId().hashCode());
211                finfo.setCreationDateTime(m_resource.getDateCreated());
212                finfo.setChangeDateTime(fdate);
213                return finfo;
214            } else {
215
216                //  Fill in a file information object for this directory
217
218                int fattr = FileAttribute.Directory;
219                if (m_cms.getRequestContext().getCurrentProject().isOnlineProject()) {
220                    fattr += FileAttribute.ReadOnly;
221                }
222                // Can't use negative file size here, since this stops Windows 7 from connecting
223                FileInfo finfo = new FileInfo(m_resource.getName(), 1, fattr);
224                long fdate = m_resource.getDateLastModified();
225                finfo.setModifyDateTime(fdate);
226                finfo.setAllocationSize(1);
227                finfo.setFileId(m_resource.getStructureId().hashCode());
228                finfo.setCreationDateTime(m_resource.getDateCreated());
229                finfo.setChangeDateTime(fdate);
230                return finfo;
231
232            }
233        } catch (CmsException e) {
234            throw CmsJlanDiskInterface.convertCmsException(e);
235
236        }
237    }
238
239    /**
240     * Moves this file to a different path.<p>
241     *
242     * @param cmsNewPath the new path
243     * @throws CmsException if something goes wrong
244     */
245    public void moveTo(String cmsNewPath) throws CmsException {
246
247        ensureLock();
248        m_cms.moveResource(m_cms.getSitePath(m_resource), cmsNewPath);
249        CmsUUID id = m_resource.getStructureId();
250        CmsResource updatedRes = m_cms.readResource(id, CmsJlanDiskInterface.STANDARD_FILTER);
251        m_resource = updatedRes;
252        updateFromResource();
253    }
254
255    /**
256     * @see org.alfresco.jlan.server.filesys.NetworkFile#openFile(boolean)
257     */
258    @Override
259    public void openFile(boolean arg0) {
260
261        // not needed
262
263    }
264
265    /**
266     * @see org.alfresco.jlan.server.filesys.NetworkFile#readFile(byte[], int, int, long)
267     */
268    @Override
269    public int readFile(byte[] buffer, int length, int bufferOffset, long fileOffset) throws IOException {
270
271        try {
272            load(true);
273            int result = m_buffer.read(buffer, length, bufferOffset, (int)fileOffset);
274            return result;
275        } catch (CmsException e) {
276            throw CmsJlanDiskInterface.convertCmsException(e);
277        }
278    }
279
280    /**
281     * Collects all files matching the given name pattern and search attributes.<p>
282     *
283     * @param name the name pattern
284     * @param searchAttributes the search attributes
285     *
286     * @return the list of file objects which match the given parameters
287     *
288     * @throws IOException if something goes wrong
289     */
290    public List<CmsJlanNetworkFile> search(String name, int searchAttributes) throws IOException {
291
292        try {
293            load(false);
294            if (m_resource.isFolder()) {
295                List<CmsJlanNetworkFile> result = new ArrayList<CmsJlanNetworkFile>();
296                String regex = WildCard.convertToRegexp(name);
297                Pattern pattern = Pattern.compile(regex);
298                List<CmsResource> children = m_cms.getResourcesInFolder(
299                    m_cms.getSitePath(m_resource),
300                    CmsJlanDiskInterface.STANDARD_FILTER);
301                for (CmsResource child : children) {
302                    CmsJlanNetworkFile childFile = new CmsJlanNetworkFile(m_cms, child, getFullChildPath(child));
303                    if (!matchesSearchAttributes(searchAttributes)) {
304                        continue;
305                    }
306                    if (!pattern.matcher(child.getName()).matches()) {
307                        continue;
308                    }
309
310                    result.add(childFile);
311                }
312                return result;
313            } else {
314                throw new AccessDeniedException("Can't search a non-directory!");
315            }
316        } catch (CmsException e) {
317            throw CmsJlanDiskInterface.convertCmsException(e);
318        }
319    }
320
321    /**
322     * @see org.alfresco.jlan.server.filesys.NetworkFile#seekFile(long, int)
323     */
324    @Override
325    public long seekFile(long pos, int typ) throws IOException {
326
327        try {
328            load(true);
329            switch (typ) {
330
331                //  From current position
332
333                case SeekType.CurrentPos:
334                    m_buffer.seek(m_buffer.getPosition() + pos);
335                    break;
336
337                //  From end of file
338
339                case SeekType.EndOfFile:
340                    long newPos = m_buffer.getLength() + pos;
341                    m_buffer.seek(newPos);
342                    break;
343
344                //  From start of file
345
346                case SeekType.StartOfFile:
347                default:
348                    m_buffer.seek(pos);
349                    break;
350            }
351            return m_buffer.getPosition();
352        } catch (CmsException e) {
353            throw new IOException(e);
354        }
355    }
356
357    /**
358     * Sets the file information.<p>
359     *
360     * @param info the file information to set
361     */
362    public void setFileInformation(FileInfo info) {
363
364        if (info.hasSetFlag(FileInfo.FlagDeleteOnClose)) {
365            setDeleteOnClose(true);
366        }
367    }
368
369    /**
370     * @see org.alfresco.jlan.server.filesys.NetworkFile#truncateFile(long)
371     */
372    @Override
373    public void truncateFile(long size) throws IOException {
374
375        try {
376            load(true);
377            m_buffer.truncate((int)size);
378            incrementWriteCount();
379        } catch (CmsException e) {
380            throw CmsJlanDiskInterface.convertCmsException(e);
381        }
382    }
383
384    /**
385     * @see org.alfresco.jlan.server.filesys.NetworkFile#writeFile(byte[], int, int, long)
386     */
387    @Override
388    public void writeFile(byte[] data, int len, int pos, long offset) throws IOException {
389
390        try {
391            if (m_resource.isFolder()) {
392                throw new AccessDeniedException("Can't write data to folder!");
393            }
394            load(true);
395            m_buffer.seek(offset);
396            byte[] dataToWrite = Arrays.copyOfRange(data, pos, pos + len);
397            m_buffer.write(dataToWrite);
398            incrementWriteCount();
399        } catch (CmsException e) {
400            throw CmsJlanDiskInterface.convertCmsException(e);
401        }
402    }
403
404    /**
405     * Make sure that this resource is locked.<p>
406     *
407     * @throws CmsException if something goes wrong
408     */
409    protected void ensureLock() throws CmsException {
410
411        CmsLock lock = m_cms.getLock(m_resource);
412        if (lock.isUnlocked() || !lock.isLockableBy(m_cms.getRequestContext().getCurrentUser())) {
413            m_cms.lockResourceTemporary(m_cms.getSitePath(m_resource));
414            m_needToUnlock = true;
415        }
416    }
417
418    /**
419     * Gets the CmsFile instance for this file, or null if the file contents haven'T been loaded already.<p>
420     *
421     * @return the CmsFile instance
422     */
423    protected CmsFile getFile() {
424
425        if (m_resource instanceof CmsFile) {
426            return (CmsFile)m_resource;
427        }
428        return null;
429    }
430
431    /**
432     * Adds the name of a child resource to this file's path.<p>
433     *
434     * @param child the child resource
435     *
436     * @return the path of the child
437     */
438    protected String getFullChildPath(CmsResource child) {
439
440        String childName = child.getName();
441        String sep = getFullName().endsWith("\\") ? "" : "\\";
442        return getFullName() + sep + childName;
443    }
444
445    /**
446     * Loads the file data from the VFS.<p>
447     *
448     * @param needContent true if we need the file content to be loaded
449     *
450     * @throws IOException if an IO error happens
451     * @throws CmsException if a CMS operation fails
452     */
453    protected void load(boolean needContent) throws IOException, CmsException {
454
455        try {
456            if (m_resource.isFolder() && needContent) {
457                throw new AccessDeniedException("Operation not supported for directories!");
458            }
459            if (m_resource.isFile() && needContent && (!(m_resource instanceof CmsFile))) {
460                m_resource = m_cms.readFile(m_cms.getSitePath(m_resource), CmsJlanDiskInterface.STANDARD_FILTER);
461            }
462            if (!m_bufferInitialized && (getFile() != null)) {
463                // readResource may already have returned a CmsFile, this is why we need to initialize the buffer
464                // here and not in the if-block above
465                m_buffer.init(getFile().getContents());
466                m_bufferInitialized = true;
467            }
468        } catch (CmsException e) {
469            throw e;
470        }
471    }
472
473    /**
474     * Checks if this file matches the given search attributes.<p>
475     *
476     * @param attributes the search attributes
477     *
478     * @return true if this file matches the search attributes given
479     */
480    protected boolean matchesSearchAttributes(int attributes) {
481
482        if (isDirectory()) {
483            return (attributes & FileAttribute.Directory) != 0;
484        } else {
485            return true;
486        }
487    }
488
489    /**
490     * Copies state information from the internal CmsResource object to this object.<p>
491     */
492    protected void updateFromResource() {
493
494        setCreationDate(m_resource.getDateCreated());
495        int length = m_resource.getLength();
496        if (m_resource.isFolder()) {
497            length = 1;
498        }
499        setFileSize(length);
500        setModifyDate(m_resource.getDateLastModified());
501        setAttributes(m_resource.isFile() ? FileAttribute.Normal : FileAttribute.Directory);
502    }
503
504    /**
505     * Replace sequences of consecutive slashes/backslashes to a single backslash.<p>
506     *
507     * @param fullName the path to normalize
508     * @return the normalized path
509     */
510    private String normalizeName(String fullName) {
511
512        return fullName.replaceAll("[/\\\\]+", "\\\\");
513    }
514
515    /**
516     * Removes trailing NUL bytes from a byte array.
517     *
518     * @param content the content
519     * @return the modified content
520     */
521    private byte[] removeTrailingNulBytes(byte[] content) {
522
523        if (content.length == 0) {
524            return content;
525        }
526        int pos = content.length - 1;
527        while ((pos >= 0) && (content[pos] == 0)) {
528            pos -= 1;
529        }
530        if (pos < 0) {
531            return new byte[] {};
532        }
533        int len = pos + 1;
534        byte[] result = new byte[len];
535        System.arraycopy(content, 0, result, 0, len);
536        return result;
537
538    }
539
540}