Package com.gitblit.tickets

Source Code of com.gitblit.tickets.FileTicketService

/*
* Copyright 2014 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gitblit.tickets;

import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.eclipse.jgit.lib.Repository;

import com.gitblit.Constants;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.IUserManager;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Attachment;
import com.gitblit.models.TicketModel.Change;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.StringUtils;

/**
* Implementation of a ticket service based on a directory within the repository.
* All tickets are serialized as a list of JSON changes and persisted in a hashed
* directory structure, similar to the standard git loose object structure.
*
* @author James Moger
*
*/
public class FileTicketService extends ITicketService {

  private static final String JOURNAL = "journal.json";

  private static final String TICKETS_PATH = "tickets/";

  private final Map<String, AtomicLong> lastAssignedId;

  public FileTicketService(
      IRuntimeManager runtimeManager,
      IPluginManager pluginManager,
      INotificationManager notificationManager,
      IUserManager userManager,
      IRepositoryManager repositoryManager) {

    super(runtimeManager,
        pluginManager,
        notificationManager,
        userManager,
        repositoryManager);

    lastAssignedId = new ConcurrentHashMap<String, AtomicLong>();
  }

  @Override
  public FileTicketService start() {
    return this;
  }

  @Override
  protected void resetCachesImpl() {
    lastAssignedId.clear();
  }

  @Override
  protected void resetCachesImpl(RepositoryModel repository) {
    if (lastAssignedId.containsKey(repository.name)) {
      lastAssignedId.get(repository.name).set(0);
    }
  }

  @Override
  protected void close() {
  }

  /**
   * Returns the ticket path. This follows the same scheme as Git's object
   * store path where the first two characters of the hash id are the root
   * folder with the remaining characters as a subfolder within that folder.
   *
   * @param ticketId
   * @return the root path of the ticket content in the ticket directory
   */
  private String toTicketPath(long ticketId) {
    StringBuilder sb = new StringBuilder();
    sb.append(TICKETS_PATH);
    long m = ticketId % 100L;
    if (m < 10) {
      sb.append('0');
    }
    sb.append(m);
    sb.append('/');
    sb.append(ticketId);
    return sb.toString();
  }

  /**
   * Returns the path to the attachment for the specified ticket.
   *
   * @param ticketId
   * @param filename
   * @return the path to the specified attachment
   */
  private String toAttachmentPath(long ticketId, String filename) {
    return toTicketPath(ticketId) + "/attachments/" + filename;
  }

  /**
   * Ensures that we have a ticket for this ticket id.
   *
   * @param repository
   * @param ticketId
   * @return true if the ticket exists
   */
  @Override
  public boolean hasTicket(RepositoryModel repository, long ticketId) {
    boolean hasTicket = false;
    Repository db = repositoryManager.getRepository(repository.name);
    try {
      String journalPath = toTicketPath(ticketId) + "/" + JOURNAL;
      hasTicket = new File(db.getDirectory(), journalPath).exists();
    } finally {
      db.close();
    }
    return hasTicket;
  }

  @Override
  public synchronized Set<Long> getIds(RepositoryModel repository) {
    Set<Long> ids = new TreeSet<Long>();
    Repository db = repositoryManager.getRepository(repository.name);
    try {
      // identify current highest ticket id by scanning the paths in the tip tree
      File dir = new File(db.getDirectory(), TICKETS_PATH);
      dir.mkdirs();
      List<File> journals = findAll(dir, JOURNAL);
      for (File journal : journals) {
        // Reconstruct ticketId from the path
        // id/26/326/journal.json
        String path = FileUtils.getRelativePath(dir, journal);
        String tid = path.split("/")[1];
        long ticketId = Long.parseLong(tid);
        ids.add(ticketId);
      }
    } finally {
      if (db != null) {
        db.close();
      }
    }
    return ids;
  }

  /**
   * Assigns a new ticket id.
   *
   * @param repository
   * @return a new long id
   */
  @Override
  public synchronized long assignNewId(RepositoryModel repository) {
    long newId = 0L;
    Repository db = repositoryManager.getRepository(repository.name);
    try {
      if (!lastAssignedId.containsKey(repository.name)) {
        lastAssignedId.put(repository.name, new AtomicLong(0));
      }
      AtomicLong lastId = lastAssignedId.get(repository.name);
      if (lastId.get() <= 0) {
        Set<Long> ids = getIds(repository);
        for (long id : ids) {
          if (id > lastId.get()) {
            lastId.set(id);
          }
        }
      }

      // assign the id and touch an empty journal to hold it's place
      newId = lastId.incrementAndGet();
      String journalPath = toTicketPath(newId) + "/" + JOURNAL;
      File journal = new File(db.getDirectory(), journalPath);
      journal.getParentFile().mkdirs();
      journal.createNewFile();
    } catch (IOException e) {
      log.error("failed to assign ticket id", e);
      return 0L;
    } finally {
      db.close();
    }
    return newId;
  }

  /**
   * Returns all the tickets in the repository. Querying tickets from the
   * repository requires deserializing all tickets. This is an  expensive
   * process and not recommended. Tickets are indexed by Lucene and queries
   * should be executed against that index.
   *
   * @param repository
   * @param filter
   *            optional filter to only return matching results
   * @return a list of tickets
   */
  @Override
  public List<TicketModel> getTickets(RepositoryModel repository, TicketFilter filter) {
    List<TicketModel> list = new ArrayList<TicketModel>();

    Repository db = repositoryManager.getRepository(repository.name);
    try {
      // Collect the set of all json files
      File dir = new File(db.getDirectory(), TICKETS_PATH);
      List<File> journals = findAll(dir, JOURNAL);

      // Deserialize each ticket and optionally filter out unwanted tickets
      for (File journal : journals) {
        String json = null;
        try {
          json = new String(FileUtils.readContent(journal), Constants.ENCODING);
        } catch (Exception e) {
          log.error(null, e);
        }
        if (StringUtils.isEmpty(json)) {
          // journal was touched but no changes were written
          continue;
        }
        try {
          // Reconstruct ticketId from the path
          // id/26/326/journal.json
          String path = FileUtils.getRelativePath(dir, journal);
          String tid = path.split("/")[1];
          long ticketId = Long.parseLong(tid);
          List<Change> changes = TicketSerializer.deserializeJournal(json);
          if (ArrayUtils.isEmpty(changes)) {
            log.warn("Empty journal for {}:{}", repository, journal);
            continue;
          }
          TicketModel ticket = TicketModel.buildTicket(changes);
          ticket.project = repository.projectPath;
          ticket.repository = repository.name;
          ticket.number = ticketId;

          // add the ticket, conditionally, to the list
          if (filter == null) {
            list.add(ticket);
          } else {
            if (filter.accept(ticket)) {
              list.add(ticket);
            }
          }
        } catch (Exception e) {
          log.error("failed to deserialize {}/{}\n{}",
              new Object [] { repository, journal, e.getMessage()});
          log.error(null, e);
        }
      }

      // sort the tickets by creation
      Collections.sort(list);
      return list;
    } finally {
      db.close();
    }
  }

  private List<File> findAll(File dir, String filename) {
    List<File> list = new ArrayList<File>();
    File [] files = dir.listFiles();
    if (files == null) {
      return list;
    }
    for (File file : files) {
      if (file.isDirectory()) {
        list.addAll(findAll(file, filename));
      } else if (file.isFile()) {
        if (file.getName().equalsIgnoreCase(filename)) {
          list.add(file);
        }
      }
    }
    return list;
  }

  /**
   * Retrieves the ticket from the repository.
   *
   * @param repository
   * @param ticketId
   * @return a ticket, if it exists, otherwise null
   */
  @Override
  protected TicketModel getTicketImpl(RepositoryModel repository, long ticketId) {
    Repository db = repositoryManager.getRepository(repository.name);
    try {
      List<Change> changes = getJournal(db, ticketId);
      if (ArrayUtils.isEmpty(changes)) {
        log.warn("Empty journal for {}:{}", repository, ticketId);
        return null;
      }
      TicketModel ticket = TicketModel.buildTicket(changes);
      if (ticket != null) {
        ticket.project = repository.projectPath;
        ticket.repository = repository.name;
        ticket.number = ticketId;
      }
      return ticket;
    } finally {
      db.close();
    }
  }

  /**
   * Retrieves the journal for the ticket.
   *
   * @param repository
   * @param ticketId
   * @return a journal, if it exists, otherwise null
   */
  @Override
  protected List<Change> getJournalImpl(RepositoryModel repository, long ticketId) {
    Repository db = repositoryManager.getRepository(repository.name);
    try {
      List<Change> changes = getJournal(db, ticketId);
      if (ArrayUtils.isEmpty(changes)) {
        log.warn("Empty journal for {}:{}", repository, ticketId);
        return null;
      }
      return changes;
    } finally {
      db.close();
    }
  }

  /**
   * Returns the journal for the specified ticket.
   *
   * @param db
   * @param ticketId
   * @return a list of changes
   */
  private List<Change> getJournal(Repository db, long ticketId) {
    if (ticketId <= 0L) {
      return new ArrayList<Change>();
    }

    String journalPath = toTicketPath(ticketId) + "/" + JOURNAL;
    File journal = new File(db.getDirectory(), journalPath);
    if (!journal.exists()) {
      return new ArrayList<Change>();
    }

    String json = null;
    try {
      json = new String(FileUtils.readContent(journal), Constants.ENCODING);
    } catch (Exception e) {
      log.error(null, e);
    }
    if (StringUtils.isEmpty(json)) {
      return new ArrayList<Change>();
    }
    List<Change> list = TicketSerializer.deserializeJournal(json);
    return list;
  }

  @Override
  public boolean supportsAttachments() {
    return true;
  }

  /**
   * Retrieves the specified attachment from a ticket.
   *
   * @param repository
   * @param ticketId
   * @param filename
   * @return an attachment, if found, null otherwise
   */
  @Override
  public Attachment getAttachment(RepositoryModel repository, long ticketId, String filename) {
    if (ticketId <= 0L) {
      return null;
    }

    // deserialize the ticket model so that we have the attachment metadata
    TicketModel ticket = getTicket(repository, ticketId);
    Attachment attachment = ticket.getAttachment(filename);

    // attachment not found
    if (attachment == null) {
      return null;
    }

    // retrieve the attachment content
    Repository db = repositoryManager.getRepository(repository.name);
    try {
      String attachmentPath = toAttachmentPath(ticketId, attachment.name);
      File file = new File(db.getDirectory(), attachmentPath);
      if (file.exists()) {
        attachment.content = FileUtils.readContent(file);
        attachment.size = attachment.content.length;
      }
      return attachment;
    } finally {
      db.close();
    }
  }

  /**
   * Deletes a ticket from the repository.
   *
   * @param ticket
   * @return true if successful
   */
  @Override
  protected synchronized boolean deleteTicketImpl(RepositoryModel repository, TicketModel ticket, String deletedBy) {
    if (ticket == null) {
      throw new RuntimeException("must specify a ticket!");
    }

    boolean success = false;
    Repository db = repositoryManager.getRepository(ticket.repository);
    try {
      String ticketPath = toTicketPath(ticket.number);
      File dir = new File(db.getDirectory(), ticketPath);
      if (dir.exists()) {
        success = FileUtils.delete(dir);
      }
      success = true;
    } finally {
      db.close();
    }
    return success;
  }

  /**
   * Commit a ticket change to the repository.
   *
   * @param repository
   * @param ticketId
   * @param change
   * @return true, if the change was committed
   */
  @Override
  protected synchronized boolean commitChangeImpl(RepositoryModel repository, long ticketId, Change change) {
    boolean success = false;

    Repository db = repositoryManager.getRepository(repository.name);
    try {
      List<Change> changes = getJournal(db, ticketId);
      changes.add(change);
      String journal = TicketSerializer.serializeJournal(changes).trim();

      String journalPath = toTicketPath(ticketId) + "/" + JOURNAL;
      File file = new File(db.getDirectory(), journalPath);
      file.getParentFile().mkdirs();
      FileUtils.writeContent(file, journal);
      success = true;
    } catch (Throwable t) {
      log.error(MessageFormat.format("Failed to commit ticket {0,number,0} to {1}",
          ticketId, db.getDirectory()), t);
    } finally {
      db.close();
    }
    return success;
  }

  @Override
  protected boolean deleteAllImpl(RepositoryModel repository) {
    Repository db = repositoryManager.getRepository(repository.name);
    try {
      File dir = new File(db.getDirectory(), TICKETS_PATH);
      return FileUtils.delete(dir);
    } catch (Exception e) {
      log.error(null, e);
    } finally {
      db.close();
    }
    return false;
  }

  @Override
  protected boolean renameImpl(RepositoryModel oldRepository, RepositoryModel newRepository) {
    return true;
  }

  @Override
  public String toString() {
    return getClass().getSimpleName();
  }
}
TOP

Related Classes of com.gitblit.tickets.FileTicketService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.