001package gu.sql2java;
002
003import java.io.ByteArrayInputStream;
004import java.io.IOException;
005import java.io.InputStream;
006import java.lang.reflect.InvocationTargetException;
007import java.net.MalformedURLException;
008import java.net.URL;
009import java.net.URLConnection;
010import java.net.URLStreamHandler;
011import java.nio.ByteBuffer;
012import java.util.Map;
013
014import com.google.common.base.Strings;
015import com.google.common.base.Throwables;
016
017import gu.sql2java.exception.ObjectRetrievalException;
018import gu.sql2java.store.BaseURLStore;
019import gu.sql2java.store.ConditionChecks;
020import gu.sql2java.store.DataNotFoundException;
021
022import static com.google.common.base.URLParseChecks.checkURLParse;
023import static com.google.common.base.Preconditions.*;
024
025import static gu.sql2java.store.BinaryUtils.*;
026import static gu.sql2java.store.ConditionChecks.checkNotNull;
027import static gu.sql2java.store.ConditionChecks.checkTrue;
028
029public class BaseColumnStore extends BaseURLStore {
030
031        private final Handler handler = new Handler();
032
033        protected final  String tablename;
034
035        protected final String storeColumn;
036
037        protected final TableManager<BaseBean> manager;
038
039        protected final RowMetaData metaData;
040
041        protected final Class<?> storeColumnType;
042
043        protected final String protocol;
044
045        protected final int extensionId;
046
047        protected final int mimeId;
048
049        protected final static String PKS = "PKS";
050        
051        public BaseColumnStore(String tablename,String storeColumn, String extensionColumn, String mimeColumn) {
052                this.tablename = tablename;
053                this.storeColumn = storeColumn;
054                this.manager = Managers.managerOf(tablename);
055                this.metaData = RowMetaData.getMetaData(tablename);
056                this.extensionId = metaData.columnIDOf(extensionColumn);
057                this.mimeId = metaData.columnIDOf(mimeColumn);
058                this.storeColumnType = metaData.columnTypeOf(storeColumn);
059                checkArgument(storeColumnType != null, "INVALID column %s", storeColumn);
060                checkArgument(byte[].class.equals(storeColumnType) || ByteBuffer.class.isAssignableFrom(storeColumnType),
061                                "INVALID column type of %s,byte[] or java.nio.ByteBuffer required",storeColumn);
062                checkArgument(extensionColumn == null || String.class.equals(metaData.columnTypeOf(this.extensionId)),
063                                "INVALID extensionColumn %s",extensionColumn);
064                checkArgument(mimeColumn == null || String.class.equals(metaData.columnTypeOf(this.mimeId)),
065                                "INVALID mimeColumn %s",mimeColumn);
066                this.protocol = tablename.replaceAll("_", "-");
067        }
068
069        @Override
070        public String getProtocol() {
071                return protocol;
072        }
073        protected Object[] primaryKeysOf(String md5, String extension){
074                Map<String, Object> p = additionalParams.get();
075                checkState(p != null && p.containsKey(PKS), "NOT PK defined");
076                Object v = p.get(PKS);
077                checkState(v instanceof Object[],"INVALID PK type,Object[] required");
078                return (Object[])v;
079        }
080        protected final String pathOf(String suffix, Object... primaryKeys){
081                StringBuffer buffer = new StringBuffer(storeColumn);
082                for(int i=0; i < primaryKeys.length;++i){
083                        buffer.append("/").append(primaryKeys[i]);
084                }
085                if(!Strings.isNullOrEmpty(suffix)){
086                        buffer.append('.').append(suffix);
087                }
088                return buffer.toString();
089        }
090        protected final URL makeURL(String suffix, Object... primaryKeys){
091                try {
092                        return new URL(getProtocol(),null,-1,pathOf(suffix,primaryKeys));
093                } catch (MalformedURLException e) {
094                        throw new RuntimeException(e);
095                }
096        }
097        /**
098         * 从数据库记录中获取当前数据的扩展名
099         * @param bean 数据库记录(不为{@code null})
100         * @return 扩展名
101         */
102        protected String getExtension(BaseBean bean) {
103                return bean.getValue(extensionId);
104        }
105
106        /**
107         * 从数据库记录中获取当前数据的MIME类型
108         * @param bean 数据库记录(不为{@code null})
109         * @return MIME类型
110         */
111        protected String getMime(BaseBean bean) {
112                String mime = bean.getValue(mimeId);
113                String extension = bean.getValue(extensionId);
114                if(mime != null){
115                        return mime;
116                } else if( extension != null){
117                        mime = URLConnection.guessContentTypeFromName("." + extension);
118                        if(mime != null){
119                                return mime;
120                        }
121                }
122                return guessContentType(bean);
123        }
124
125        private String guessContentType(BaseBean bean) {
126                try {
127                        if(bean == null){
128                                return null;
129                        }
130                        byte[] data = getBytes(bean.getValue(storeColumn));
131                        if(data == null){
132                                return null;
133                        }
134                        ByteArrayInputStream is = new ByteArrayInputStream(data);
135                        return URLConnection.guessContentTypeFromStream(is);
136                } catch (IOException e) {
137                        throw new RuntimeException(e);
138                }
139        }
140
141        protected final void fillStoreBean(BaseBean bean, byte[] binary, String extension) {
142                if(ByteBuffer.class.equals(storeColumnType)){
143                        bean.setValue(storeColumn, ByteBuffer.wrap(binary));
144                }else{
145                        bean.setValue(storeColumn, binary);     
146                }
147                if(extensionId >= 0){
148                        bean.setValue(extensionId, extension);
149                }
150                if(mimeId >= 0){
151                        bean.setValue(mimeId, getMime(bean));
152                }
153        }
154
155        
156        @Override
157        protected URL doStore(byte[] binary, String md5, String extension, boolean makeURLOnly) throws IOException {
158                try {
159                        Object[] pks = primaryKeysOf(md5,extension);
160                        BaseBean bean = manager.loadByPrimaryKeyChecked(pks);
161                        fillStoreBean(bean, binary, extension);
162                        if(!makeURLOnly){
163                                manager.save(bean);
164                        }
165                        return makeURL(extension, pks);
166                } catch (Exception e) {
167                        Throwables.throwIfInstanceOf(e, IOException.class);
168                        throw new IOException(e);
169                } finally{
170                        additionalParams.remove();
171                }
172        }
173
174        @Override
175        protected URL doFind(String md5) {
176                return null;
177        }
178
179        @Override
180        protected boolean doExists(URL storedURL) {
181                DatabaseURLConnection connection = new DatabaseURLConnection(storedURL).parse();
182                BaseBean found = manager.loadByPrimaryKey(connection.primaryKeys);
183                return found != null && found.getValue(storeColumn) != null;
184        }
185
186        @Override
187        protected boolean doDelete(URL storedURL) throws IOException {
188                try {
189                        DatabaseURLConnection bean = new DatabaseURLConnection(storedURL).parse();
190                        return manager.deleteByPrimaryKey(bean.primaryKeys) == 1;
191                } catch (Exception e) {
192                        Throwables.throwIfInstanceOf(e, IOException.class);
193                        throw new IOException(e);
194                }
195        }
196
197        @Override
198        protected URLStreamHandler doGetURLStreamHandler() {
199                return handler;
200        }
201
202        @Override
203        public int hashCode() {
204                final int prime = 31;
205                int result = super.hashCode();
206                result = prime * result + extensionId;
207                result = prime * result + mimeId;
208                result = prime * result + ((protocol == null) ? 0 : protocol.hashCode());
209                result = prime * result + ((storeColumn == null) ? 0 : storeColumn.hashCode());
210                result = prime * result + ((tablename == null) ? 0 : tablename.hashCode());
211                return result;
212        }
213
214        @Override
215        public boolean equals(Object obj) {
216                if (this == obj)
217                        return true;
218                if (!super.equals(obj))
219                        return false;
220                if (!(obj instanceof BaseColumnStore))
221                        return false;
222                BaseColumnStore other = (BaseColumnStore) obj;
223                if (extensionId != other.extensionId)
224                        return false;
225                if (mimeId != other.mimeId)
226                        return false;
227                if (protocol == null) {
228                        if (other.protocol != null)
229                                return false;
230                } else if (!protocol.equals(other.protocol))
231                        return false;
232                if (storeColumn == null) {
233                        if (other.storeColumn != null)
234                                return false;
235                } else if (!storeColumn.equals(other.storeColumn))
236                        return false;
237                if (tablename == null) {
238                        if (other.tablename != null)
239                                return false;
240                } else if (!tablename.equals(other.tablename))
241                        return false;
242                return true;
243        }
244
245        @Override
246        public String toString() {
247                StringBuilder builder = new StringBuilder();
248                builder.append(getClass().getSimpleName() + " [tablename=");
249                builder.append(tablename);
250                builder.append(", storeColumn=");
251                builder.append(storeColumn);
252                builder.append(", extensionColumn=");
253                builder.append(metaData.columnNameOf(extensionId));
254                builder.append(", mimeColumn=");
255                builder.append(metaData.columnNameOf(mimeId));
256                builder.append(", protocol=");
257                builder.append(protocol);
258                builder.append("]");
259                return builder.toString();
260        }
261
262        protected class Handler extends URLStreamHandler{
263        
264                @Override
265                protected URLConnection openConnection(URL u) throws IOException {
266                                return new DatabaseURLConnection(u);    
267                }
268                
269        }
270        protected class DatabaseURLConnection extends URLConnection{
271                
272                Object[] primaryKeys;
273
274                protected DatabaseURLConnection(URL url) {
275                        super(url);
276                }
277                /**
278                 * 解析URL为数据访问信息<br>
279                 * 要求的URL格式 ${tablename}://${storeColumn}/${pk}...[/${pkN}]
280                 * @return
281                 * @throws URLParseException
282                 */
283                DatabaseURLConnection parse() throws URLParseException{
284                        checkTrue(getProtocol().equals(getURL().getProtocol()), URLParseException.class, 
285                                        "INVALID protocol ,%s reqired",getProtocol());
286                        String[] components = url.getPath().split("/");
287                        try {
288                                checkTrue(components[0].equals(storeColumn), URLParseException.class, 
289                                                "INVALID URL,the URL's path be reqired to start with '%s'",storeColumn);
290                                checkTrue(components.length == metaData.primaryKeyCount+1, URLParseException.class, 
291                                                "MISMATCH path components acount, %s required",metaData.primaryKeyCount+1);
292                                String last = components[components.length-1];
293                                int lastDot = last.lastIndexOf(".");
294                                if(lastDot >= 0){
295                                        // remove suffix if exists
296                                        components[components.length-1] = last.substring(0, lastDot);
297                                }                               
298                                primaryKeys = new Object[metaData.primaryKeyTypes.length];
299                                for(int i = 0; i < metaData.primaryKeyTypes.length;++i){
300                                        primaryKeys[i] = valueOf(components[i+1], metaData.primaryKeyTypes[i]);
301                                }
302                        } catch (IndexOutOfBoundsException e) {
303                                throw new URLParseException("URL's path lack the necessary components /${storeColumn}/${pk}...[/${pkN}]", e);
304                        } catch (StringCastException e) {
305                                throw new URLParseException("FAIL TO get primary key from URL's path", e);
306                        }
307                        checkURLParse(url != null, "NOT DEFINED store column name in ref(#)");
308                        Class<?> columnType = checkNotNull(metaData.columnTypeOf(storeColumn),URLParseException.class,"INVALID column %s",storeColumn);
309                        checkURLParse(byte[].class.equals(columnType) || ByteBuffer.class.isAssignableFrom(columnType),
310                                        "INVALID column type of %s, binary type ( byte[] or java.nio.ByteBuffer) required",storeColumn);
311                        return this;
312                }
313
314                @Override
315                public void connect() throws IOException {                      
316                        try {
317                                parse();
318                                connected = true;
319                        } catch (Exception e) {
320                                Throwables.throwIfInstanceOf(e, IOException.class);
321                                throw new IOException(e);
322                        }                       
323                }
324                @Override
325        public InputStream getInputStream() throws IOException{
326            connect();
327                        try {
328                                BaseBean bean = manager.loadByPrimaryKeyChecked(primaryKeys);
329                                Object data = bean.getValue(storeColumn);
330                                ConditionChecks.checkTrue(data != null, DataNotFoundException.class, "column %s of %s is null", storeColumn, url.toString());
331                                return new ByteArrayInputStream(getBytes(data));
332                        } catch (ObjectRetrievalException e) {
333                                throw new DataNotFoundException(url,e);
334                        }catch (Exception e) {
335                                Throwables.throwIfInstanceOf(e, IOException.class);
336                                throw new IOException(e);
337                        }               
338        }
339        }
340        @SuppressWarnings("unchecked")
341        private static <T> T valueOf(String input,Class<T> targetType)throws StringCastException{
342                        if(Strings.isNullOrEmpty(input) || String.class.equals(targetType)){
343                                return (T) input;
344                        }
345                        checkNotNull(targetType != null, StringCastException.class,"target is null");
346                        try {
347                                return (T) targetType.getMethod("valueOf", String.class).invoke(null, input);
348                        } catch (IllegalAccessException | NoSuchMethodException | SecurityException e) {
349                        } catch (InvocationTargetException e) {
350                                throw new StringCastException(e.getTargetException());
351                        }
352                        throw new StringCastException(String.format("INVALID target type %s",targetType));
353        }
354        public static class URLParseException extends RuntimeException{
355                private static final long serialVersionUID = 1L;
356
357                public URLParseException(Throwable cause) {
358                        super(cause);
359                }
360
361                public URLParseException(String message, Throwable cause) {
362                        super(message, cause);
363                }
364
365                public URLParseException(String message) {
366                        super(message);
367                }
368        }
369        public static class StringCastException extends RuntimeException{
370                private static final long serialVersionUID = 1L;
371
372                public StringCastException(Throwable cause) {
373                        super(cause);
374                }
375
376                public StringCastException(String message) {
377                        super(message);
378                }
379        }
380}
381