code: /*
   * Computoser is a music-composition algorithm and a website to present the results
   * Copyright (C) 2012-2014  Bozhidar Bozhanov
   *
   * Computoser is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Affero General Public License as
   * published by the Free Software Foundation, either version 3 of the
   * License, or (at your option) any later version.
   *
   * Computoser is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Affero General Public License for more details.
   *
   * You should have received a copy of the GNU Affero General Public License
   * along with Computoser.  If not, see <http://www.gnu.org/licenses/>.
   */
  
  package com.music.service;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.Collection;
  import java.util.List;
  import java.util.Map;
  import java.util.Random;
  import java.util.concurrent.Callable;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipOutputStream;
  
  import javax.annotation.Resource;
  import javax.inject.Inject;
  
  import jm.constants.Instruments;
  import jm.music.data.Score;
  import jm.util.Write;
  
  import org.apache.commons.io.IOUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.joda.time.DateTime;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.cache.annotation.Cacheable;
  import org.springframework.jmx.export.annotation.ManagedOperation;
  import org.springframework.jmx.export.annotation.ManagedOperationParameter;
  import org.springframework.jmx.export.annotation.ManagedResource;
  import org.springframework.scheduling.annotation.Async;
  import org.springframework.stereotype.Service;
  import org.springframework.transaction.annotation.Transactional;
  
  import com.google.common.base.Joiner;
  import com.music.Generator;
  import com.music.MusicXmlRenderer;
  import com.music.ScoreContext;
  import com.music.dao.PieceDao;
  import com.music.model.PartType;
  import com.music.model.persistent.Piece;
  import com.music.model.persistent.PieceEvaluation;
  import com.music.model.persistent.PiecePlay;
  import com.music.model.persistent.User;
  import com.music.model.prefs.Tempo;
  import com.music.model.prefs.UserPreferences;
  import com.music.util.MutingPrintStream;
  import com.music.util.RetryableOperation;
  import com.music.util.SharedData;
  import com.music.util.music.SMFTools;
  
  @Service
  @ManagedResource
  public class PieceService {
  
      private static final Logger logger = LoggerFactory.getLogger(PieceService.class);
  
      private Random random = new Random();
  
      @Inject
      private Generator generator;
  
      @Inject
      private SharedData sharedData;
  
      @Inject
      private PieceDao dao;
  
      @Resource(name = "${filesystem.implementation}")
      private FileStorageService fileStorageService;
  
      @Value("${storage.dir}")
      private String storageDir;
  
      @Value("${algorithm.version}")
      private String algorithmVersion;
  
      private Joiner joiner = Joiner.on(",");
  
      @Transactional
      public long generatePiece() {
          long start = System.currentTimeMillis();
          ScoreContext ctx = generator.generatePiece();
          logger.info("Piece generation took: " + (System.currentTimeMillis() - start) + " millis");
          byte[] mp3 = null;
          byte[] midi = null;
          try {
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              try {
                  start = System.currentTimeMillis();
                  MutingPrintStream.ignore.set(true);
                  Write.midi(ctx.getScore(), baos);
                  logger.info("Writing MIDI in memory took: " + (System.currentTimeMillis() - start) + " millis");
              } finally {
                  MutingPrintStream.ignore.set(null);
              }
              midi = baos.toByteArray();
              mp3 = generator.toMp3(midi);
          } catch (Exception e) {
              throw new IllegalStateException(e);
          }
          Piece piece = savePiece(ctx, mp3, midi);
  
          return piece.getId();
      }
  
      @Transactional(readOnly = true)
      public long getRandomTopPieceId() {
          // return the if of a random existing top 200 track
          List<Piece> topPieces = dao.getTopPieces(0, 200);
          return topPieces.get(random.nextInt(topPieces.size())).getId();
      }
  
      @Transactional
      public void evaluate(long pieceId, Long userId, boolean positive, String ip) {
          if (dao.getEvaluation(pieceId, userId, ip) == null) {
              PieceEvaluation evaluation = new PieceEvaluation();
              evaluation.setDateTime(new DateTime());
              if (userId != null) {
                  evaluation.setUser(dao.getById(User.class, userId));
              }
              Piece piece = dao.getById(Piece.class, pieceId);
              evaluation.setPiece(piece);
              evaluation.setPositive(positive);
              evaluation.setIp(ip);
              dao.persist(evaluation);
  
              // not exact due to race condition - a job should count likes every
              // X minutes
              if (positive) {
                  piece.setLikes(piece.getLikes() + 1);
              } else {
                  piece.setLikes(piece.getLikes() - 1);
              }
          }
      }
  
      private Piece savePiece(ScoreContext ctx, byte[] mp3, byte[] midi) {
          Piece piece = new Piece();
          piece.setNewlyCreated(true);
          piece.setGenerationTime(new DateTime());
          piece.setTitle(ctx.getScore().getTitle());
          piece.setKeyNote(ctx.getKeyNote());
          piece.setMeasures(ctx.getMeasures());
          piece.setNormalizedMeasureSize(ctx.getNormalizedMeasureSize());
          piece.setTempo((int) ctx.getScore().getTempo());
          piece.setNoteLengthCoefficient(ctx.getNoteLengthCoefficient());
          piece.setScale(ctx.getScale());
          piece.setAlternativeScale(ctx.getAlternativeScale());
          piece.setUpBeatLength(ctx.getUpBeatLength());
          piece.setMetreNumerator(ctx.getMetre()[0]);
          piece.setMetreDenominator(ctx.getMetre()[1]);
          piece.setMainInstrument(ctx.getParts().get(PartType.MAIN).getInstrument());
          piece.setVariation(ctx.getVariation());
          piece.setParts(joiner.join(ctx.getParts().keySet()));
          piece.setAlgorithmVersion(algorithmVersion);
  
          piece.getIntermediateDecisions().setDullBass(ctx.isDullBass());
          piece.getIntermediateDecisions().setFourToTheFloor(ctx.isFourToTheFloor());
          piece.getIntermediateDecisions().setSimplePhrases(ctx.isSimplePhrases());
          piece.getIntermediateDecisions().setAccompaniment(ctx.getParts().containsKey(PartType.ACCOMPANIMENT));
          piece.getIntermediateDecisions().setDrums(ctx.getParts().containsKey(PartType.PERCUSSIONS));
          piece.getIntermediateDecisions().setClassical(
                  ctx.getParts().get(PartType.MAIN).getInstrument() == Instruments.PIANO
                          && piece.getIntermediateDecisions().isAccompaniment());
          piece.getIntermediateDecisions().setTempoType(Tempo.forValue(piece.getTempo()));
          piece.getIntermediateDecisions().setElectronic(ctx.isElectronic());
          piece.getIntermediateDecisions().setDissonant(ctx.isDissonant());
          piece.getIntermediateDecisions().setOrnamented(ctx.isOrnamented());
          piece.getIntermediateDecisions().setVariations(StringUtils.left(joiner.join(ctx.getVariations()), 2000));
  
          Piece result = dao.persist(piece);
  
          try {
              fileStorageService.storeFile(getMp3FilePath(result.getId()), mp3);
              fileStorageService.storeFile(getMidiFilePath(result.getId()), midi);
          } catch (IOException e) {
              throw new IllegalStateException(e);
          }
          return result;
      }
  
      @Transactional
      public void storePlay(long pieceId, Long userId, String ip, boolean mobileApp) {
          PiecePlay play = new PiecePlay();
          play.setDateTime(new DateTime());
          play.setMobileApp(mobileApp);
          if (userId != null) {
              play.setUser(dao.getById(User.class, userId));
          }
          Piece piece = dao.getById(Piece.class, pieceId);
          play.setPiece(piece);
          play.setIp(ip);
          dao.persist(play);
  
          if (piece.isNewlyCreated()) {
              // we don't care about isolation here - 2 or more users can get (and
              // set new=false to) the same piece and that's OK
              piece.setNewlyCreated(false);
              dao.persist(piece);
          }
      }
  
      @Transactional(readOnly = true)
      public List<Piece> getUserPieces(Long id, int page) {
          return dao.getUserPieces(id, page, 30);
      }
  
      @Transactional(readOnly = true)
      @Cacheable(value = "topPiecesCache")
      public List<Piece> getTopPieces(int page) {
          return dao.getTopPieces(page, 30);
      }
  
      @Transactional(readOnly = true)
      @Cacheable(value = "topRecentPiecesCache")
      public Map<Piece, Integer> getTopRecentPieces(int page) {
          return dao.getTopRecentPieces(page, 30, new DateTime().minusWeeks(2));
      }
  
      public InputStream getPieceFile(final long id) {
          RetryableOperation<InputStream> op = RetryableOperation.create(new Callable<InputStream>() {
              @Override
              public InputStream call() throws Exception {
                  try (InputStream is = fileStorageService.getFile(getMp3FilePath(id))) {
                      return new ByteArrayInputStream(IOUtils.toByteArray(is));
                  }
              }
  
          });
          try {
              return op.retry(3, IOException.class);
          } catch (IOException ex) {
              throw new IllegalStateException(ex);
          }
      }
  
      public InputStream getPieceMidiFile(long id) {
          try (InputStream is = fileStorageService.getFile(getMidiFilePath(id))) {
              return new ByteArrayInputStream(IOUtils.toByteArray(is));
          } catch (IOException ex) {
              throw new IllegalStateException(ex);
          }
      }
  
      public InputStream getPieceMusicXml(long id) throws IOException {
          Piece piece = dao.getById(Piece.class, id);
          try (InputStream is = fileStorageService.getFile(getMidiFilePath(id))) {
              ByteArrayOutputStream result = new ByteArrayOutputStream();
              Score score = new Score();
              SMFTools localSMF = new SMFTools();
              localSMF.read(is);
              SMFTools.SMFToScore(score, localSMF);
              score.setTitle(piece.getTitle());
              score.setNumerator(piece.getMetreNumerator());
              score.setDenominator(piece.getMetreDenominator());
              MusicXmlRenderer.render(score, result);
              return new ByteArrayInputStream(result.toByteArray());
          }
      }
  
      private String getMp3FilePath(long id) {
          return storageDir + id + ".mp3";
      }
  
      private String getMidiFilePath(long id) {
          return storageDir + id + ".mid";
      }
  
      @Transactional(readOnly = true)
      public Piece getPiece(long id) {
          return dao.getById(Piece.class, id);
      }
  
      @Transactional
      public long getNextPieceId(UserPreferences preferences) {
          Piece piece = dao.getNewlyCreatedPiece(preferences);
          long id = 1;
          if (piece == null && preferences.isDefault()) {
              id = getRandomPieceId();
          } else if (piece == null && !preferences.isDefault()) {
              List<Piece> pieces = dao.getByPreferences(preferences);
              if (!pieces.isEmpty()) {
                  id = pieces.get(random.nextInt(pieces.size())).getId();
              } else {
                  id = getRandomPieceId();
              }
          } else {
              id = piece.getId();
          }
          sharedData.getListeningRequests().incrementAndGet();
          return id;
      }
  
      private long getRandomPieceId() {
          Piece piece;
          long maxId = sharedData.getMaxId();
          if (maxId == 0) {
              maxId = dao.getMaxPieceId();
          }
          long id = 0;
          int attempts = 0;
          while (attempts < 5) {
              id = (long) (1 + (Math.random() * maxId));
              piece = dao.getById(Piece.class, id);
              if (piece != null && piece.getLikes() >= -3) {
                  break;
              }
              attempts++;
          }
          return id;
      }
  
      @ManagedOperation
      @Async
      public void generatePieces(int count) {
          for (int i = 0; i < count; i++) {
              generatePiece();
          }
      }
  
      @ManagedOperation
      @Async
      public void rerenderMp3(@ManagedOperationParameter(description = "", name = "fromId") int fromId,
              @ManagedOperationParameter(description = "", name = "toId") int toId) {
          for (int i = fromId; i <= toId; i++) {
              try (InputStream is = fileStorageService.getFile(getMidiFilePath(i))) {
                  byte[] midi = IOUtils.toByteArray(is);
                  byte[] mp3 = generator.toMp3(midi);
                  fileStorageService.storeFile(getMp3FilePath(i), mp3);
              } catch (Exception ex) {
                  logger.warn("Error rerendering midi with id=" + i);
              }
          }
      }
  
      @Cacheable(value = "rssCache")
      public List<Piece> getRssFeed() {
          DateTime now = new DateTime();
          DateTime tenDaysAgo = now.minusDays(10);
          return dao.getFeedEntryPiecesInRange(tenDaysAgo, now);
      }
  
      @Transactional(readOnly = true)
      public List<Piece> getPieces(List<Long> pieceIds) {
          return dao.getByIds(Piece.class, pieceIds);
      }
  
      @Transactional
      public void incrementDownloads(long id, boolean midi) {
          Piece piece = dao.getById(Piece.class, id);
          if (midi) {
              piece.setMidiDownloads(piece.getMidiDownloads() + 1);
          } else {
              piece.setMp3Downloads(piece.getMp3Downloads() + 1);
          }
          dao.persist(piece);
      }
  
      @Transactional(readOnly = true)
      public List<Piece> search(UserPreferences preferences) {
          List<Piece> result = dao.getByPreferences(preferences);
          return result;
      }
  
      @Transactional
      public void save(Piece piece) {
          dao.persist(piece);
      }
  
      public void downloadPieces(OutputStream out, Collection<Piece> pieces) throws IOException {
          ZipOutputStream zip = new ZipOutputStream(out);
          for (Piece piece : pieces) {
              ZipEntry entry = new ZipEntry(piece.getTitle() + "-" + piece.getId() + ".mp3");
              zip.putNextEntry(entry);
              zip.write(IOUtils.toByteArray(getPieceFile(piece.getId())));
              zip.closeEntry();
  
              entry = new ZipEntry(piece.getTitle() + "-" + piece.getId() + ".midi");
              zip.putNextEntry(entry);
              zip.write(IOUtils.toByteArray(getPieceMidiFile(piece.getId())));
              zip.closeEntry();
          }
  
          ZipEntry license = new ZipEntry("license.txt");
          zip.putNextEntry(license);
          zip.write(IOUtils.toByteArray(getClass().getResourceAsStream("/emailTemplates/license.txt")));
          zip.closeEntry();
          zip.finish();
      }
  }
  