View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  
20  package davmail.exchange.graph;
21  
22  import davmail.exception.DavMailException;
23  import davmail.exchange.VProperty;
24  import davmail.util.DateUtil;
25  import davmail.util.StringUtil;
26  import org.apache.log4j.Logger;
27  import org.codehaus.jettison.json.JSONArray;
28  import org.codehaus.jettison.json.JSONException;
29  import org.codehaus.jettison.json.JSONObject;
30  
31  import java.text.ParseException;
32  import java.text.SimpleDateFormat;
33  import java.util.Date;
34  import java.util.HashSet;
35  import java.util.ResourceBundle;
36  import java.util.TimeZone;
37  
38  import static davmail.util.DateUtil.CALDAV_DATE_TIME;
39  import static davmail.util.DateUtil.GRAPH_DATE_TIME;
40  
41  /**
42   * Wrapper for Graph API JsonObject
43   */
44  public class GraphObject {
45      protected static final Logger LOGGER = Logger.getLogger(GraphObject.class);
46  
47      protected final JSONObject jsonObject;
48      protected int statusCode;
49  
50      public GraphObject(JSONObject jsonObject) {
51          this.jsonObject = jsonObject;
52      }
53  
54      public String optString(String key) {
55          // use field mapping to get value
56          return optString(GraphField.get(key));
57      }
58  
59      public String optString(GraphField field) {
60          String key = field.getGraphId();
61          if (key == null) {
62              return null;
63          }
64          String value = null;
65          // special case for keywords/categories
66          if ("keywords".equals(key) || "categories".equals(key)) {
67              JSONArray categoriesArray = jsonObject.optJSONArray("categories");
68              HashSet<String> keywords = new HashSet<>();
69              // Collects keywords from the categories array, joins into comma‑separated string
70              if (categoriesArray != null) {
71                  for (int j = 0; j < categoriesArray.length(); j++) {
72                      keywords.add(categoriesArray.optString(j));
73                  }
74                  value = StringUtil.join(keywords, ",");
75              }
76          } else if ("from".equals(key)) {
77              value = formatEmailAddress(jsonObject.optJSONObject(key));
78          } else if ("@odata.etag".equals(key)) {
79              // tasks don't have an etag field, use @odata.etag
80              String odataEtag = jsonObject.optString("@odata.etag");
81              if (odataEtag != null && odataEtag.startsWith("W/\"") && odataEtag.endsWith("\"")) {
82                  value = odataEtag.substring(3, odataEtag.length() - 1);
83              }
84          } else if (!field.isExtended()) {
85              // grab value by key
86              value = jsonObject.optString(key, null);
87          } else {
88              JSONArray singleValueExtendedProperties = jsonObject.optJSONArray("singleValueExtendedProperties");
89              if (singleValueExtendedProperties != null) {
90                  // Iterates extended properties to find a matching value
91                  for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
92                      JSONObject singleValueObject = singleValueExtendedProperties.optJSONObject(i);
93                      if (singleValueObject != null && key.equals(singleValueObject.optString("id"))) {
94                          value = singleValueObject.optString("value");
95                      }
96                  }
97              }
98          }
99          if (field.isDate()) {
100             try {
101                 value = GraphExchangeSession.convertDateFromExchange(value);
102             } catch (DavMailException e) {
103                 LOGGER.warn("Invalid date " + value + " on field " + key);
104             }
105         }
106         return value;
107     }
108 
109     protected String formatEmailAddress(JSONObject jsonObject) {
110         String value = null;
111         if (jsonObject != null) {
112             JSONObject jsonEmailAddress = jsonObject.optJSONObject("emailAddress");
113             if (jsonEmailAddress != null) {
114                 value = jsonEmailAddress.optString("name", "") + " <" + jsonEmailAddress.optString("address", "") + ">";
115             }
116         }
117         return value;
118     }
119 
120     public JSONArray optJSONArray(String key) {
121         return jsonObject.optJSONArray(key);
122     }
123 
124     /**
125      * Set value for alias.
126      * First map property alias to a field to determine graph id, then set graph property or singleValueExtendedProperty
127      * @param alias field alias
128      * @param value property value
129      * @throws JSONException on error
130      */
131     public void put(String alias, String value) throws JSONException {
132         GraphField field = GraphField.get(alias);
133         String key = field.getGraphId();
134         // assume all expanded properties have a space
135         if (field.isExtended()) {
136             // force number attributes value
137             if (field.isNumber() && value == null) {
138                 value = "0";
139             }
140             if (field.isBoolean() && value == null) {
141                 value = "false";
142             }
143             getSingleValueExtendedProperties().put(new JSONObject().put("id", key).put("value", value == null ? JSONObject.NULL : value));
144         } else {
145             jsonObject.put(key, value == null ? JSONObject.NULL : value);
146         }
147     }
148 
149     /**
150      * Set the boolean value on the field defined by alias.
151      * First map property alias to a field to determine graph id, then set graph property or singleValueExtendedProperty
152      * @param alias field alias
153      * @param value property value
154      * @throws JSONException on error
155      */
156     public void put(String alias, boolean value) throws JSONException {
157         GraphField field = GraphField.get(alias);
158         String key = field.getGraphId();
159         // extended field values go under singleValueExtendedProperties
160         if (field.isExtended()) {
161             getSingleValueExtendedProperties().put(new JSONObject().put("id", key).put("value", value));
162         } else {
163             jsonObject.put(key, value);
164         }
165     }
166 
167     public void put(String key, JSONArray values) throws JSONException {
168         jsonObject.put(key, values);
169     }
170 
171     /**
172      * Set categories from comma separated values.
173      * @param values comma separated values
174      * @throws JSONException on error
175      */
176     public void setCategories(String values) throws JSONException {
177         if (values != null) {
178             setCategories(values.split(","));
179         } else {
180             jsonObject.put("categories", new JSONArray());
181         }
182     }
183 
184     public void setCategories(String[] values) throws JSONException {
185         // assume all expanded properties have a space
186         JSONArray jsonValues = new JSONArray();
187         for (String singleValue : values) {
188             jsonValues.put(singleValue);
189         }
190         jsonObject.put("categories", jsonValues);
191     }
192 
193 
194     /**
195      * Get a singleValueExtendedProperties JSON array.
196      * Create an empty JSON array on the first call.
197      * @return singleValueExtendedProperties array
198      * @throws JSONException on error
199      */
200     protected JSONArray getSingleValueExtendedProperties() throws JSONException {
201         JSONArray singleValueExtendedProperties = jsonObject.optJSONArray("singleValueExtendedProperties");
202         if (singleValueExtendedProperties == null) {
203             singleValueExtendedProperties = new JSONArray();
204             jsonObject.put("singleValueExtendedProperties", singleValueExtendedProperties);
205         }
206         return singleValueExtendedProperties;
207     }
208 
209 
210     /**
211      * Get mandatory property value.
212      * @param key property name
213      * @return value
214      * @throws JSONException on missing property
215      */
216     public String getString(String key) throws JSONException {
217         String value = optString(key);
218         if (value == null) {
219             throw new JSONException("JSONObject[" + key + "] not found.");
220         }
221         return value;
222     }
223 
224     /**
225      * Get optional parameter from JSON property.
226      * @param section JSON property name
227      * @param key internal property name
228      * @return value or null
229      */
230     public String optString(String section, String key) {
231         JSONObject sectionObject = jsonObject.optJSONObject(section);
232         if (sectionObject != null) {
233             return sectionObject.optString(key, null);
234         }
235         return null;
236     }
237 
238     public boolean getBoolean(String key) throws JSONException {
239         return getBoolean(GraphField.get(key));
240     }
241 
242     public boolean optBoolean(String key) {
243         return optBoolean(GraphField.get(key));
244     }
245 
246     public boolean getBoolean(GraphField field) throws JSONException {
247         String key = field.getGraphId();
248         if (key == null) {
249             return false;
250         }
251         return jsonObject.getBoolean(key);
252     }
253 
254     public boolean optBoolean(GraphField field) {
255         String key = field.getGraphId();
256         if (key == null) {
257             return false;
258         }
259         return jsonObject.optBoolean(key);
260     }
261 
262     public JSONObject optJSONObject(String key) {
263         return jsonObject.optJSONObject(key);
264     }
265 
266     /**
267      * Convert datetimetimezone to java date.
268      * Graph API returns some dates as json object with dateTime and timeZone, see
269      * <a href="https://learn.microsoft.com/en-us/graph/api/resources/datetimetimezone">datetimetimezone</a>
270      * @param key property key, e.g. dueDateTime
271      * @return java date
272      * @throws DavMailException on error
273      */
274     public Date optDateTimeTimeZone(String key) throws DavMailException {
275         JSONObject sectionObject = jsonObject.optJSONObject(key);
276         if (sectionObject != null) {
277             String timeZone = sectionObject.optString("timeZone");
278             String dateTime = sectionObject.optString("dateTime");
279             if (timeZone != null && dateTime != null) {
280                 try {
281                     SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS");
282                     parser.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(timeZone)));
283                     return parser.parse(dateTime);
284                 } catch (ParseException e) {
285                     throw new DavMailException("EXCEPTION_INVALID_DATE", dateTime);
286                 }
287             }
288         }
289         return null;
290     }
291 
292     /**
293      * Compute recurrenceId property based on the original start and timezone.
294      * @return recurrenceId property
295      * @throws DavMailException on error
296      */
297     public VProperty getRecurrenceId() throws DavMailException {
298         // get the unmodified start date and timezone of occurrence
299         String originalStartTimeZone = optString("originalStartTimeZone");
300         String originalStart = optString("originalStart");
301 
302         if (originalStartTimeZone != null && originalStart != null && originalStart.length() >= 19) {
303             // Convert date from graph to caldav format, keep timezone information
304             VProperty recurrenceId = new VProperty("RECURRENCE-ID", DateUtil.convertDateFormat(originalStart.substring(0, 19), GRAPH_DATE_TIME, CALDAV_DATE_TIME));
305             recurrenceId.setParam("TZID", originalStartTimeZone);
306             return recurrenceId;
307         } else {
308             throw new DavMailException("LOG_MESSAGE", "Missing original start date and timezone");
309         }
310     }
311 
312     /**
313      * Convert Exchange timezone id to standard timezone id.
314      * Standard timezones use the area/location format.
315      * @param exchangeTimezone Exchange / O365 timezone id
316      * @return standard timezone
317      */
318     public static String convertTimezoneFromExchange(String exchangeTimezone) {
319         ResourceBundle tzidsBundle = ResourceBundle.getBundle("stdtimezones");
320         if (tzidsBundle.containsKey(exchangeTimezone)) {
321             return tzidsBundle.getString(exchangeTimezone);
322         } else {
323             return exchangeTimezone;
324         }
325     }
326 
327 
328     public int optInt(String key) {
329         return jsonObject.optInt(key);
330     }
331 
332     public String toString(int indentFactor) throws JSONException {
333         return jsonObject.toString(indentFactor);
334     }
335 }
336