1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.io.BufferedInputStream;
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileOutputStream;
25 import java.io.FilenameFilter;
26 import java.io.IOException;
27 import java.io.ObjectInputStream;
28 import java.io.ObjectOutputStream;
29 import java.io.OutputStream;
30 import java.util.Arrays;
31 import java.util.Calendar;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.zip.GZIPInputStream;
35 import java.util.zip.GZIPOutputStream;
36
37 import net.bull.javamelody.Parameter;
38 import net.bull.javamelody.internal.common.LOG;
39 import net.bull.javamelody.internal.common.Parameters;
40
41 /**
42  * Classe chargée de l'enregistrement et de la lecture d'un {@link Counter}.
43  * @author Emeric Vernat
44  */

45 public class CounterStorage {
46     private static final int DEFAULT_OBSOLETE_STATS_DAYS = 365;
47     private static boolean storageDisabled;
48     private final Counter counter;
49
50     // do not user CounterResponseStream to not depend on the net.bull.internal.web package
51     private static class CounterOutputStream extends OutputStream {
52         int dataLength;
53         private final OutputStream output;
54
55         CounterOutputStream(OutputStream output) {
56             super();
57             this.output = output;
58         }
59
60         @Override
61         public void write(int b) throws IOException {
62             output.write(b);
63             dataLength++;
64         }
65
66         @Override
67         public void write(byte[] b) throws IOException {
68             output.write(b);
69             dataLength += b.length;
70         }
71
72         @Override
73         public void write(byte[] b, int off, int len) throws IOException {
74             output.write(b, off, len);
75             dataLength += len;
76         }
77
78         @Override
79         public void flush() throws IOException {
80             output.flush();
81         }
82
83         @Override
84         public void close() throws IOException {
85             output.close();
86         }
87     }
88
89     /**
90      * Constructeur.
91      * @param counter Counter
92      */

93     CounterStorage(Counter counter) {
94         super();
95         assert counter != null;
96         this.counter = counter;
97     }
98
99     /**
100      * Enregistre le counter.
101      * @return Taille sérialisée non compressée du counter (estimation pessimiste de l'occupation mémoire)
102      * @throws IOException Exception d'entrée/sortie
103      */

104     int writeToFile() throws IOException {
105         if (storageDisabled) {
106             return -1;
107         }
108         final File file = getFile();
109         if (counter.getRequestsCount() == 0 && counter.getErrorsCount() == 0 && !file.exists()) {
110             // s'il n'y a pas de requête, inutile d'écrire des fichiers de compteurs vides
111             // (par exemple pour le compteur ejb s'il n'y a pas d'ejb)
112             return -1;
113         }
114         final File directory = file.getParentFile();
115         if (!directory.mkdirs() && !directory.exists()) {
116             throw new IOException("JavaMelody directory can't be created: " + directory.getPath());
117         }
118         return writeToFile(counter, file);
119     }
120
121     static int writeToFile(Counter counter, File file) throws IOException {
122         try (FileOutputStream out = new FileOutputStream(file)) {
123             final CounterOutputStream counterOutput = new CounterOutputStream(
124                     new GZIPOutputStream(new BufferedOutputStream(out)));
125             try (ObjectOutputStream output = new ObjectOutputStream(counterOutput)) {
126                 output.writeObject(counter);
127                 // ce close libère les ressources du ObjectOutputStream et du GZIPOutputStream
128             }
129             // retourne la taille sérialisée non compressée,
130             // qui est une estimation pessimiste de l'occupation mémoire
131             return counterOutput.dataLength;
132         }
133     }
134
135     /**
136      * Lecture du counter depuis son fichier et retour du résultat.
137      * @return Counter
138      * @throws IOException e
139      */

140     Counter readFromFile() throws IOException {
141         if (storageDisabled) {
142             return null;
143         }
144         final File file = getFile();
145         if (file.exists()) {
146             return readFromFile(file);
147         }
148         // ou on retourne null si le fichier n'existe pas
149         return null;
150     }
151
152     static Counter readFromFile(File file) throws IOException {
153         try (FileInputStream in = new FileInputStream(file)) {
154             try (ObjectInputStream input = TransportFormat
155                     .createObjectInputStream(new GZIPInputStream(new BufferedInputStream(in)))) {
156                 // on retourne l'instance du counter lue
157                 return (Counter) input.readObject();
158                 // ce close libère les ressources du ObjectInputStream et du GZIPInputStream
159             }
160         } catch (final ClassNotFoundException e) {
161             throw new IOException(e.getMessage(), e);
162         } catch (final IllegalStateException | ClassCastException e) {
163             LOG.warn("could not deserialize " + file.getName()
164                     + " , corrupted file will be deleted.", e);
165             file.delete();
166             return null;
167         }
168     }
169
170     private File getFile() {
171         final File storageDirectory = Parameters.getStorageDirectory(counter.getApplication());
172         return new File(storageDirectory, counter.getStorageName() + ".ser.gz");
173     }
174
175     static long deleteObsoleteCounterFiles(String application) {
176         final Calendar nowMinusOneYearAndADay = Calendar.getInstance();
177         nowMinusOneYearAndADay.add(Calendar.DAY_OF_YEAR, -getObsoleteStatsDays());
178         nowMinusOneYearAndADay.add(Calendar.DAY_OF_YEAR, -1);
179         // filtre pour ne garder que les fichiers d'extension .ser.gz et pour éviter d'instancier des File inutiles
180         long diskUsage = 0;
181         for (final File file : listSerGzFiles(application)) {
182             boolean deleted = false;
183             if (file.lastModified() < nowMinusOneYearAndADay.getTimeInMillis()) {
184                 deleted = file.delete();
185             }
186             if (!deleted) {
187                 diskUsage += file.length();
188             }
189         }
190
191         // on retourne true si tous les fichiers .ser.gz obsolètes ont été supprimés, false sinon
192         return diskUsage;
193     }
194
195     /**
196      * @return Nombre de jours avant qu'un fichier de statistiques (extension .ser.gz),
197      * soit considéré comme obsolète et soit supprimé automatiquement, à minuit (365 par défaut, soit 1 an)
198      */

199     private static int getObsoleteStatsDays() {
200         final String param = Parameter.OBSOLETE_STATS_DAYS.getValue();
201         if (param != null) {
202             // lance une NumberFormatException si ce n'est pas un nombre
203             final int result = Integer.parseInt(param);
204             if (result <= 0) {
205                 throw new IllegalStateException(
206                         "The parameter obsolete-stats-days should be > 0 (365 recommended)");
207             }
208             return result;
209         }
210         return DEFAULT_OBSOLETE_STATS_DAYS;
211     }
212
213     private static List<File> listSerGzFiles(String application) {
214         final File storageDir = Parameters.getStorageDirectory(application);
215         // filtre pour ne garder que les fichiers d'extension .rrd et pour éviter d'instancier des File inutiles
216         final FilenameFilter filenameFilter = new FilenameFilter() {
217             /** {@inheritDoc} */
218             @Override
219             public boolean accept(File dir, String fileName) {
220                 return fileName.endsWith(".ser.gz");
221             }
222         };
223         final File[] files = storageDir.listFiles(filenameFilter);
224         if (files == null) {
225             return Collections.emptyList();
226         }
227         return Arrays.asList(files);
228     }
229
230     // cette méthode est utilisée dans l'ihm Swing
231     public static void disableStorage() {
232         storageDisabled = true;
233     }
234 }
235